From 4614c9ce1fa9cdd237c38c2c55d24bee85d7b98d Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Sun, 16 Aug 2026 07:44:41 +0200 Subject: [PATCH 1/2] fix(flag-evaluation): prevent prototype pollution --- .changeset/safe-ravens-unflatten.md | 5 ++++ packages/flag-evaluation/src/index.ts | 9 ++++++- packages/flag-evaluation/test/index.test.ts | 27 +++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .changeset/safe-ravens-unflatten.md 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..0a40d9515 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -1202,6 +1202,33 @@ 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.hasOwn(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", From 3eff26e8db7813c6511813dbe964d481fef5eec3 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Sun, 16 Aug 2026 08:01:30 +0200 Subject: [PATCH 2/2] test(flag-evaluation): use compatible own-property check --- packages/flag-evaluation/test/index.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 0a40d9515..87fa138eb 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -1220,7 +1220,9 @@ describe("unflattenJSON", () => { expect(output).toEqual({}); for (const property of unexpectedProperties) { - expect(Object.hasOwn(Object.prototype, property)).toBe(false); + expect( + Object.prototype.hasOwnProperty.call(Object.prototype, property), + ).toBe(false); } } finally { for (const property of unexpectedProperties) {