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 objectUtils from 'objectUtils';
|
||||||
|
import { filter__proto__ } from 'utils/sanitization';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
export default class ImportAsJSONAction {
|
export default class ImportAsJSONAction {
|
||||||
@ -71,8 +72,10 @@ export default class ImportAsJSONAction {
|
|||||||
|
|
||||||
onSave(object, changes) {
|
onSave(object, changes) {
|
||||||
const selectFile = changes.selectFile;
|
const selectFile = changes.selectFile;
|
||||||
const objectTree = selectFile.body;
|
const jsonTree = selectFile.body;
|
||||||
this._importObjectTree(object, JSON.parse(objectTree));
|
const objectTree = JSON.parse(jsonTree, filter__proto__);
|
||||||
|
|
||||||
|
this._importObjectTree(object, objectTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
|
|
||||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
|
|
||||||
import ImportFromJSONAction from './ImportFromJSONAction';
|
|
||||||
|
|
||||||
let openmct;
|
let openmct;
|
||||||
let importFromJSONAction;
|
let importFromJSONAction;
|
||||||
|
let folderObject;
|
||||||
|
let unObserve;
|
||||||
|
|
||||||
describe('The import JSON action', function () {
|
describe('The import JSON action', function () {
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
@ -34,19 +34,8 @@ describe('The import JSON action', function () {
|
|||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.startHeadless();
|
openmct.startHeadless();
|
||||||
|
|
||||||
importFromJSONAction = new ImportFromJSONAction(openmct);
|
importFromJSONAction = openmct.actions.getAction('import.JSON');
|
||||||
});
|
folderObject = {
|
||||||
|
|
||||||
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 = {
|
|
||||||
composition: [],
|
composition: [],
|
||||||
name: 'Unnamed Folder',
|
name: 'Unnamed Folder',
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
@ -59,8 +48,23 @@ describe('The import JSON action', function () {
|
|||||||
key: '84438cda-a071-48d1-b9bf-d77bd53e59ba'
|
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);
|
spyOn(openmct.composition, 'get').and.returnValue(true);
|
||||||
|
|
||||||
@ -97,21 +101,7 @@ describe('The import JSON action', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls showForm on invoke ', function () {
|
it('calls showForm on invoke ', function () {
|
||||||
const domainObject = {
|
const objectPath = [folderObject];
|
||||||
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];
|
|
||||||
|
|
||||||
spyOn(openmct.forms, 'showForm').and.returnValue(Promise.resolve({}));
|
spyOn(openmct.forms, 'showForm').and.returnValue(Promise.resolve({}));
|
||||||
spyOn(importFromJSONAction, 'onSave').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();
|
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.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { filter__proto__ } from '../../utils/sanitization';
|
||||||
|
|
||||||
export default class LocalStorageObjectProvider {
|
export default class LocalStorageObjectProvider {
|
||||||
constructor(spaceKey = 'mct') {
|
constructor(spaceKey = 'mct') {
|
||||||
this.localStorage = window.localStorage;
|
this.localStorage = window.localStorage;
|
||||||
@ -83,7 +85,7 @@ export default class LocalStorageObjectProvider {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
getSpaceAsObject() {
|
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);
|
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(() => {
|
afterEach(() => {
|
||||||
resetApplicationState(openmct);
|
resetApplicationState(openmct);
|
||||||
resetLocalStorage();
|
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