Protect against prototype pollution in import action (#7094)

This commit is contained in:
David Tsay 2023-10-02 14:50:53 -07:00 committed by GitHub
parent 3c7d3397d6
commit 2243381d52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 113 additions and 34 deletions

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import objectUtils from 'objectUtils';
import { filter__proto__ } from 'utils/sanitization';
import { v4 as uuid } from 'uuid';
export default class ImportAsJSONAction {
@ -71,8 +72,10 @@ export default class ImportAsJSONAction {
onSave(object, changes) {
const selectFile = changes.selectFile;
const objectTree = selectFile.body;
this._importObjectTree(object, JSON.parse(objectTree));
const jsonTree = selectFile.body;
const objectTree = JSON.parse(jsonTree, filter__proto__);
this._importObjectTree(object, objectTree);
}
/**

View File

@ -22,10 +22,10 @@
import { createOpenMct, resetApplicationState } from 'utils/testing';
import ImportFromJSONAction from './ImportFromJSONAction';
let openmct;
let importFromJSONAction;
let folderObject;
let unObserve;
describe('The import JSON action', function () {
beforeEach((done) => {
@ -34,19 +34,8 @@ describe('The import JSON action', function () {
openmct.on('start', done);
openmct.startHeadless();
importFromJSONAction = new ImportFromJSONAction(openmct);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('has import as JSON action', () => {
expect(importFromJSONAction.key).toBe('import.JSON');
});
it('applies to return true for objects with composition', function () {
const domainObject = {
importFromJSONAction = openmct.actions.getAction('import.JSON');
folderObject = {
composition: [],
name: 'Unnamed Folder',
type: 'folder',
@ -59,8 +48,23 @@ describe('The import JSON action', function () {
key: '84438cda-a071-48d1-b9bf-d77bd53e59ba'
}
};
});
const objectPath = [domainObject];
afterEach(() => {
importFromJSONAction = undefined;
folderObject = undefined;
unObserve?.();
unObserve = undefined;
return resetApplicationState(openmct);
});
it('has import as JSON action', () => {
expect(importFromJSONAction).toBeDefined();
});
it('applies to return true for objects with composition', function () {
const objectPath = [folderObject];
spyOn(openmct.composition, 'get').and.returnValue(true);
@ -97,21 +101,7 @@ describe('The import JSON action', function () {
});
it('calls showForm on invoke ', function () {
const domainObject = {
composition: [],
name: 'Unnamed Folder',
type: 'folder',
location: '9f6c9dae-51c3-401d-92f1-c812de942922',
modified: 1637021471624,
persisted: 1637021471624,
id: '84438cda-a071-48d1-b9bf-d77bd53e59ba',
identifier: {
namespace: '',
key: '84438cda-a071-48d1-b9bf-d77bd53e59ba'
}
};
const objectPath = [domainObject];
const objectPath = [folderObject];
spyOn(openmct.forms, 'showForm').and.returnValue(Promise.resolve({}));
spyOn(importFromJSONAction, 'onSave').and.returnValue(Promise.resolve({}));
@ -119,4 +109,37 @@ describe('The import JSON action', function () {
expect(openmct.forms.showForm).toHaveBeenCalled();
});
it('protects against prototype pollution', (done) => {
spyOn(console, 'warn');
spyOn(openmct.forms, 'showForm').and.callFake(returnResponseWithPrototypePollution);
unObserve = openmct.objects.observe(folderObject, '*', callback);
importFromJSONAction.invoke([folderObject]);
function callback(newObject) {
const hasPollutedProto =
Object.prototype.hasOwnProperty.call(newObject, '__proto__') ||
Object.prototype.hasOwnProperty.call(Object.getPrototypeOf(newObject), 'toString');
// warning from openmct.objects.get
expect(console.warn).not.toHaveBeenCalled();
expect(hasPollutedProto).toBeFalse();
done();
}
function returnResponseWithPrototypePollution() {
const pollutedResponse = {
selectFile: {
name: 'imported object',
// eslint-disable-next-line prettier/prettier
body: "{\"openmct\":{\"c28d230d-e909-4a3e-9840-d9ef469dda70\":{\"identifier\":{\"key\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[],\"configuration\":{\"series\":[]},\"modified\":1695837546833,\"location\":\"mine\",\"created\":1695837546833,\"persisted\":1695837546833,\"__proto__\":{\"toString\":\"foobar\"}}},\"rootId\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\"}"
}
};
return Promise.resolve(pollutedResponse);
}
});
});

View File

@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { filter__proto__ } from '../../utils/sanitization';
export default class LocalStorageObjectProvider {
constructor(spaceKey = 'mct') {
this.localStorage = window.localStorage;
@ -83,7 +85,7 @@ export default class LocalStorageObjectProvider {
* @private
*/
getSpaceAsObject() {
return JSON.parse(this.getSpace());
return JSON.parse(this.getSpace(), filter__proto__);
}
/**

View File

@ -73,6 +73,28 @@ describe('The local storage plugin', () => {
expect(testObject.anotherProperty).toEqual(domainObject.anotherProperty);
});
it('prevents prototype pollution from manipulated localstorage', async () => {
spyOn(console, 'warn');
const identifier = {
namespace: '',
key: 'test-key'
};
const pollutedSpaceString = `{"test-key":{"__proto__":{"toString":"foobar"},"type":"folder","name":"A test object","identifier":{"namespace":"","key":"test-key"}}}`;
getLocalStorage()[space] = pollutedSpaceString;
let testObject = await openmct.objects.get(identifier);
const hasPollutedProto =
Object.prototype.hasOwnProperty.call(testObject, '__proto__') ||
Object.getPrototypeOf(testObject) !== Object.getPrototypeOf({});
// warning from openmct.objects.get
expect(console.warn).not.toHaveBeenCalled();
expect(hasPollutedProto).toBeFalse();
});
afterEach(() => {
resetApplicationState(openmct);
resetLocalStorage();

29
src/utils/sanitization.js Normal file
View File

@ -0,0 +1,29 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
function filter__proto__(key, value) {
if (key !== '__proto__') {
return value;
}
}
export { filter__proto__ };