Skip to content

Commit

Permalink
Protect against prototype pollution in import action (#7094)
Browse files Browse the repository at this point in the history
  • Loading branch information
davetsay committed Oct 2, 2023
1 parent 3c7d339 commit 2243381
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 34 deletions.
7 changes: 5 additions & 2 deletions src/plugins/importFromJSONAction/ImportFromJSONAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*****************************************************************************/

import objectUtils from 'objectUtils';
import { filter__proto__ } from 'utils/sanitization';
import { v4 as uuid } from 'uuid';

export default class ImportAsJSONAction {
Expand Down Expand Up @@ -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);
}

/**
Expand Down
85 changes: 54 additions & 31 deletions src/plugins/importFromJSONAction/ImportFromJSONActionSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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',
Expand All @@ -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);

Expand Down Expand Up @@ -97,26 +101,45 @@ 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({}));
importFromJSONAction.invoke(objectPath);

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);
}
});
});
4 changes: 3 additions & 1 deletion src/plugins/localStorage/LocalStorageObjectProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,7 +85,7 @@ export default class LocalStorageObjectProvider {
* @private
*/
getSpaceAsObject() {
return JSON.parse(this.getSpace());
return JSON.parse(this.getSpace(), filter__proto__);
}

/**
Expand Down
22 changes: 22 additions & 0 deletions src/plugins/localStorage/pluginSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
29 changes: 29 additions & 0 deletions src/utils/sanitization.js
Original file line number Diff line number Diff line change
@@ -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__ };

0 comments on commit 2243381

Please sign in to comment.