Skip to content

Commit 8f2510d

Browse files
committed
fix: object state mutation
1 parent 7273d85 commit 8f2510d

File tree

1 file changed

+67
-9
lines changed

1 file changed

+67
-9
lines changed

src/ObjectStateMutations.ts

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,45 @@ export interface State {
1919
existed: boolean;
2020
}
2121

22+
/**
23+
* Check if a property name or path is potentially dangerous for prototype pollution
24+
* @param key
25+
*/
26+
function isDangerousKey(key: string): boolean {
27+
const dangerousKeys = ["__proto__", "constructor", "prototype"];
28+
// Check if the key itself is dangerous
29+
if (dangerousKeys.includes(key)) {
30+
return true;
31+
}
32+
// Check if any part of a dotted path is dangerous
33+
if (key.includes(".")) {
34+
const parts = key.split(".");
35+
return parts.some((part) => dangerousKeys.includes(part));
36+
}
37+
return false;
38+
}
39+
2240
export function defaultState(): State {
2341
return {
24-
serverData: {},
25-
pendingOps: [{}],
26-
objectCache: {},
42+
serverData: Object.create(null),
43+
pendingOps: [Object.create(null)],
44+
objectCache: Object.create(null),
2745
tasks: new TaskQueue(),
2846
existed: false,
2947
};
3048
}
3149

3250
export function setServerData(serverData: AttributeMap, attributes: AttributeMap) {
3351
for (const attr in attributes) {
34-
if (typeof attributes[attr] !== 'undefined') {
52+
// Skip properties from prototype chain
53+
if (!Object.prototype.hasOwnProperty.call(attributes, attr)) {
54+
continue;
55+
}
56+
// Skip dangerous keys that could pollute prototypes
57+
if (isDangerousKey(attr)) {
58+
continue;
59+
}
60+
if (typeof attributes[attr] !== "undefined") {
3561
serverData[attr] = attributes[attr];
3662
} else {
3763
delete serverData[attr];
@@ -40,6 +66,10 @@ export function setServerData(serverData: AttributeMap, attributes: AttributeMap
4066
}
4167

4268
export function setPendingOp(pendingOps: OpsMap[], attr: string, op?: Op) {
69+
// Skip dangerous keys that could pollute prototypes
70+
if (isDangerousKey(attr)) {
71+
return;
72+
}
4373
const last = pendingOps.length - 1;
4474
if (op) {
4575
pendingOps[last][attr] = op;
@@ -49,13 +79,13 @@ export function setPendingOp(pendingOps: OpsMap[], attr: string, op?: Op) {
4979
}
5080

5181
export function pushPendingState(pendingOps: OpsMap[]) {
52-
pendingOps.push({});
82+
pendingOps.push(Object.create(null));
5383
}
5484

5585
export function popPendingState(pendingOps: OpsMap[]): OpsMap {
5686
const first = pendingOps.shift();
5787
if (!pendingOps.length) {
58-
pendingOps[0] = {};
88+
pendingOps[0] = Object.create(null);
5989
}
6090
return first;
6191
}
@@ -64,6 +94,14 @@ export function mergeFirstPendingState(pendingOps: OpsMap[]) {
6494
const first = popPendingState(pendingOps);
6595
const next = pendingOps[0];
6696
for (const attr in first) {
97+
// Skip properties from prototype chain
98+
if (!Object.prototype.hasOwnProperty.call(first, attr)) {
99+
continue;
100+
}
101+
// Skip dangerous keys that could pollute prototypes
102+
if (isDangerousKey(attr)) {
103+
continue;
104+
}
67105
if (next[attr] && first[attr]) {
68106
const merged = next[attr].mergeWith(first[attr]);
69107
if (merged) {
@@ -81,6 +119,10 @@ export function estimateAttribute(
81119
object: ParseObject,
82120
attr: string
83121
): any {
122+
// Skip dangerous keys that could pollute prototypes
123+
if (isDangerousKey(attr)) {
124+
return undefined;
125+
}
84126
let value = serverData[attr];
85127
for (let i = 0; i < pendingOps.length; i++) {
86128
if (pendingOps[i][attr]) {
@@ -101,13 +143,21 @@ export function estimateAttributes(
101143
pendingOps: OpsMap[],
102144
object: ParseObject
103145
): AttributeMap {
104-
const data = {};
146+
const data = Object.create(null);
105147
let attr;
106148
for (attr in serverData) {
107149
data[attr] = serverData[attr];
108150
}
109151
for (let i = 0; i < pendingOps.length; i++) {
110152
for (attr in pendingOps[i]) {
153+
// Skip properties from prototype chain
154+
if (!Object.prototype.hasOwnProperty.call(pendingOps[i], attr)) {
155+
continue;
156+
}
157+
// Skip dangerous keys that could pollute prototypes
158+
if (isDangerousKey(attr)) {
159+
continue;
160+
}
111161
if (pendingOps[i][attr] instanceof RelationOp) {
112162
if (object.id) {
113163
data[attr] = (pendingOps[i][attr] as RelationOp).applyTo(data[attr], object, attr);
@@ -125,7 +175,7 @@ export function estimateAttributes(
125175
if (!isNaN(nextKey)) {
126176
object[key] = [];
127177
} else {
128-
object[key] = {};
178+
object[key] = Object.create(null);
129179
}
130180
} else {
131181
if (Array.isArray(object[key])) {
@@ -165,7 +215,7 @@ function nestedSet(obj, key, value) {
165215
if (!isNaN(nextPath)) {
166216
obj[path] = [];
167217
} else {
168-
obj[path] = {};
218+
obj[path] = Object.create(null);
169219
}
170220
}
171221
obj = obj[path];
@@ -184,6 +234,14 @@ export function commitServerChanges(
184234
) {
185235
const ParseObject = CoreManager.getParseObject();
186236
for (const attr in changes) {
237+
// Skip properties from prototype chain
238+
if (!Object.prototype.hasOwnProperty.call(changes, attr)) {
239+
continue;
240+
}
241+
// Skip dangerous keys that could pollute prototypes
242+
if (isDangerousKey(attr)) {
243+
continue;
244+
}
187245
const val = changes[attr];
188246
nestedSet(serverData, attr, val);
189247
if (

0 commit comments

Comments
 (0)