Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protect against prototype pollution in import action #7094

Merged
merged 9 commits into from
Oct 2, 2023
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__);
shefalijoshi marked this conversation as resolved.
Show resolved Hide resolved

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__);
shefalijoshi marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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__ };