Skip to content

Commit 338e9d2

Browse files
authored
Prevent prototype pollution in JSON unflattening (#702)
## Summary - prevent `unflattenJSON` from traversing unsafe prototype-related path segments - process only own properties from the flattened input - add regression coverage for `constructor.prototype` and `__proto__` paths - add a patch changeset for `@reflag/flag-evaluation` The Changesets release flow will also patch dependent packages, including `@reflag/node-sdk`, so they resolve the fixed evaluation package. ## Validation - `@reflag/flag-evaluation`: 145 tests passed - `@reflag/node-sdk`: 218 tests passed - TypeScript builds passed for both packages - lint and formatting checks passed
1 parent 43bde73 commit 338e9d2

3 files changed

Lines changed: 42 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@reflag/flag-evaluation": patch
3+
---
4+
5+
Prevent prototype pollution when unflattening JSON with unsafe property paths.

‎packages/flag-evaluation/src/index.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,16 @@ export function flattenJSON(data: object): Record<string, string> {
254254
*/
255255
export function unflattenJSON(data: Record<string, any>): Record<string, any> {
256256
const result: Record<string, any> = {};
257+
// Traversing these properties on a plain object can reach Object.prototype.
258+
const unsafePathSegments = new Set(["__proto__", "constructor", "prototype"]);
257259

258-
for (const i in data) {
260+
for (const i of Object.keys(data)) {
259261
const keys = i.split(".");
262+
263+
if (keys.some((key) => unsafePathSegments.has(key))) {
264+
continue;
265+
}
266+
260267
keys.reduce((acc, key, index) => {
261268
if (index === keys.length - 1) {
262269
if (typeof acc === "object") {

‎packages/flag-evaluation/test/index.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,35 @@ describe("unflattenJSON", () => {
12021202
expect(output).toEqual({});
12031203
});
12041204

1205+
it("should prevent prototype pollution", () => {
1206+
const unexpectedProperties = [
1207+
"unexpectedConstructorPathProperty",
1208+
"unexpectedProtoPathProperty",
1209+
];
1210+
1211+
for (const property of unexpectedProperties) {
1212+
delete Object.prototype[property];
1213+
}
1214+
1215+
try {
1216+
const output = unflattenJSON({
1217+
"constructor.prototype.unexpectedConstructorPathProperty": "value",
1218+
"__proto__.unexpectedProtoPathProperty": "value",
1219+
});
1220+
1221+
expect(output).toEqual({});
1222+
for (const property of unexpectedProperties) {
1223+
expect(
1224+
Object.prototype.hasOwnProperty.call(Object.prototype, property),
1225+
).toBe(false);
1226+
}
1227+
} finally {
1228+
for (const property of unexpectedProperties) {
1229+
delete Object.prototype[property];
1230+
}
1231+
}
1232+
});
1233+
12051234
it("should convert a flat object with one level deep keys to a nested object", () => {
12061235
const input = {
12071236
"a.b.c": "value",

0 commit comments

Comments
 (0)