Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions src/ObjectStateMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import TaskQueue from './TaskQueue';
import { RelationOp } from './ParseOp';
import type { Op } from './ParseOp';
import type ParseObject from './ParseObject';
import { isDangerousKey } from "./isDangerousKey";

export type AttributeMap = Record<string, any>;
export type OpsMap = Record<string, Op>;
Expand All @@ -21,17 +22,25 @@ export interface State {

export function defaultState(): State {
return {
serverData: {},
pendingOps: [{}],
objectCache: {},
serverData: Object.create(null),
pendingOps: [Object.create(null)],
objectCache: Object.create(null),
tasks: new TaskQueue(),
existed: false,
};
}

export function setServerData(serverData: AttributeMap, attributes: AttributeMap) {
for (const attr in attributes) {
if (typeof attributes[attr] !== 'undefined') {
// Skip properties from prototype chain
if (!Object.prototype.hasOwnProperty.call(attributes, attr)) {
continue;
}
// Skip dangerous keys that could pollute prototypes
if (isDangerousKey(attr)) {
continue;
}
if (typeof attributes[attr] !== "undefined") {
serverData[attr] = attributes[attr];
} else {
delete serverData[attr];
Expand All @@ -40,6 +49,10 @@ export function setServerData(serverData: AttributeMap, attributes: AttributeMap
}

export function setPendingOp(pendingOps: OpsMap[], attr: string, op?: Op) {
// Skip dangerous keys that could pollute prototypes
if (isDangerousKey(attr)) {
return;
}
const last = pendingOps.length - 1;
if (op) {
pendingOps[last][attr] = op;
Expand All @@ -49,13 +62,13 @@ export function setPendingOp(pendingOps: OpsMap[], attr: string, op?: Op) {
}

export function pushPendingState(pendingOps: OpsMap[]) {
pendingOps.push({});
pendingOps.push(Object.create(null));
}

export function popPendingState(pendingOps: OpsMap[]): OpsMap {
const first = pendingOps.shift();
if (!pendingOps.length) {
pendingOps[0] = {};
pendingOps[0] = Object.create(null);
}
return first;
}
Expand All @@ -64,6 +77,14 @@ export function mergeFirstPendingState(pendingOps: OpsMap[]) {
const first = popPendingState(pendingOps);
const next = pendingOps[0];
for (const attr in first) {
// Skip properties from prototype chain
if (!Object.prototype.hasOwnProperty.call(first, attr)) {
continue;
}
// Skip dangerous keys that could pollute prototypes
if (isDangerousKey(attr)) {
continue;
}
if (next[attr] && first[attr]) {
const merged = next[attr].mergeWith(first[attr]);
if (merged) {
Expand All @@ -81,6 +102,10 @@ export function estimateAttribute(
object: ParseObject,
attr: string
): any {
// Skip dangerous keys that could pollute prototypes
if (isDangerousKey(attr)) {
return undefined;
}
let value = serverData[attr];
for (let i = 0; i < pendingOps.length; i++) {
if (pendingOps[i][attr]) {
Expand All @@ -101,13 +126,21 @@ export function estimateAttributes(
pendingOps: OpsMap[],
object: ParseObject
): AttributeMap {
const data = {};
const data = Object.create(null);
let attr;
for (attr in serverData) {
data[attr] = serverData[attr];
}
for (let i = 0; i < pendingOps.length; i++) {
for (attr in pendingOps[i]) {
// Skip properties from prototype chain
if (!Object.prototype.hasOwnProperty.call(pendingOps[i], attr)) {
continue;
}
// Skip dangerous keys that could pollute prototypes
if (isDangerousKey(attr)) {
continue;
}
if (pendingOps[i][attr] instanceof RelationOp) {
if (object.id) {
data[attr] = (pendingOps[i][attr] as RelationOp).applyTo(data[attr], object, attr);
Expand All @@ -125,7 +158,7 @@ export function estimateAttributes(
if (!isNaN(nextKey)) {
object[key] = [];
} else {
object[key] = {};
object[key] = Object.create(null);
}
} else {
if (Array.isArray(object[key])) {
Expand Down Expand Up @@ -165,7 +198,7 @@ function nestedSet(obj, key, value) {
if (!isNaN(nextPath)) {
obj[path] = [];
} else {
obj[path] = {};
obj[path] = Object.create(null);
}
}
obj = obj[path];
Expand All @@ -184,6 +217,14 @@ export function commitServerChanges(
) {
const ParseObject = CoreManager.getParseObject();
for (const attr in changes) {
// Skip properties from prototype chain
if (!Object.prototype.hasOwnProperty.call(changes, attr)) {
continue;
}
// Skip dangerous keys that could pollute prototypes
if (isDangerousKey(attr)) {
continue;
}
const val = changes[attr];
nestedSet(serverData, attr, val);
if (
Expand Down
2 changes: 1 addition & 1 deletion src/ParseObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ type ToJSON<T> = {

// Mapping of class names to constructors, so we can populate objects from the
// server with appropriate subclasses of ParseObject
const classMap: AttributeMap = {};
const classMap: AttributeMap = Object.create(null);

// Global counter for generating unique Ids for non-single-instance objects
let objectCount = 0;
Expand Down
55 changes: 54 additions & 1 deletion src/__tests__/ObjectStateMutations-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
jest.dontMock('../decode');
jest.dontMock('../encode');
jest.dontMock('../CoreManager');
jest.dontMock('../isDangerousKey');
jest.dontMock('../ObjectStateMutations');
jest.dontMock('../ParseFile');
jest.dontMock('../ParseGeoPoint');
Expand All @@ -11,7 +12,7 @@ jest.dontMock('../TaskQueue');
const mockObject = function (className) {
this.className = className;
};
mockObject.registerSubclass = function () {};
mockObject.registerSubclass = function () { };
jest.setMock('../ParseObject', mockObject);
const CoreManager = require('../CoreManager').default;
CoreManager.setParseObject(mockObject);
Expand Down Expand Up @@ -351,4 +352,56 @@ describe('ObjectStateMutations', () => {
existed: false,
});
});

describe('Prototype Pollution Protection', () => {
beforeEach(() => {
// Clear any pollution before each test
delete Object.prototype.polluted;
delete Object.prototype.malicious;
});

afterEach(() => {
// Clean up after tests
delete Object.prototype.polluted;
delete Object.prototype.malicious;
});

it('should not pollute Object.prototype in estimateAttributes with malicious attribute names', () => {
const testObj = {};

const serverData = {};
const pendingOps = [
{
__proto__: new ParseOps.SetOp({ polluted: 'yes' }),
constructor: new ParseOps.SetOp({ malicious: 'data' }),
},
];

ObjectStateMutations.estimateAttributes(serverData, pendingOps, {
className: 'TestClass',
id: 'test123',
});

// Verify Object.prototype was not polluted
expect(testObj.polluted).toBeUndefined();
expect(testObj.malicious).toBeUndefined();
expect({}.polluted).toBeUndefined();
expect({}.malicious).toBeUndefined();
});

it('should not pollute Object.prototype in commitServerChanges with nested __proto__ path', () => {
const testObj = {};

const serverData = {};
const objectCache = {};
ObjectStateMutations.commitServerChanges(serverData, objectCache, {
'__proto__.polluted': 'exploited',
});

// Verify Object.prototype was not polluted
expect(testObj.polluted).toBeUndefined();
expect({}.polluted).toBeUndefined();
expect(Object.prototype.polluted).toBeUndefined();
});
});
});
Loading