mirror of
https://github.com/nasa/openmct.git
synced 2024-12-18 20:57:53 +00:00
Protect against prototype pollution in import action (#7094)
This commit is contained in:
parent
3c7d3397d6
commit
2243381d52
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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__);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
29
src/utils/sanitization.js
Normal 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__ };
|
Loading…
Reference in New Issue
Block a user