diff --git a/.changeset/safe-ravens-unflatten.md b/.changeset/safe-ravens-unflatten.md new file mode 100644 index 000000000..f59806d19 --- /dev/null +++ b/.changeset/safe-ravens-unflatten.md @@ -0,0 +1,5 @@ +--- +"@reflag/flag-evaluation": patch +--- + +Prevent prototype pollution when unflattening JSON with unsafe property paths. diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index a7374a3e7..2064963f9 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -254,9 +254,16 @@ export function flattenJSON(data: object): Record { */ export function unflattenJSON(data: Record): Record { const result: Record = {}; + // Traversing these properties on a plain object can reach Object.prototype. + const unsafePathSegments = new Set(["__proto__", "constructor", "prototype"]); - for (const i in data) { + for (const i of Object.keys(data)) { const keys = i.split("."); + + if (keys.some((key) => unsafePathSegments.has(key))) { + continue; + } + keys.reduce((acc, key, index) => { if (index === keys.length - 1) { if (typeof acc === "object") { diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 7f34835c7..87fa138eb 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -1202,6 +1202,35 @@ describe("unflattenJSON", () => { expect(output).toEqual({}); }); + it("should prevent prototype pollution", () => { + const unexpectedProperties = [ + "unexpectedConstructorPathProperty", + "unexpectedProtoPathProperty", + ]; + + for (const property of unexpectedProperties) { + delete Object.prototype[property]; + } + + try { + const output = unflattenJSON({ + "constructor.prototype.unexpectedConstructorPathProperty": "value", + "__proto__.unexpectedProtoPathProperty": "value", + }); + + expect(output).toEqual({}); + for (const property of unexpectedProperties) { + expect( + Object.prototype.hasOwnProperty.call(Object.prototype, property), + ).toBe(false); + } + } finally { + for (const property of unexpectedProperties) { + delete Object.prototype[property]; + } + } + }); + it("should convert a flat object with one level deep keys to a nested object", () => { const input = { "a.b.c": "value",