diff --git a/src/added.js b/src/added.js index 85e955f..00e17fd 100644 --- a/src/added.js +++ b/src/added.js @@ -1,4 +1,4 @@ -import { isEmpty, isObject, hasOwnProperty } from './utils.js'; +import { isEmpty, isObject, hasOwnProperty, makeObjectWithoutPrototype } from './utils.js'; const addedDiff = (lhs, rhs) => { @@ -19,7 +19,7 @@ const addedDiff = (lhs, rhs) => { acc[key] = r[key]; return acc; - }, {}); + }, makeObjectWithoutPrototype()); }; export default addedDiff; diff --git a/src/deleted.js b/src/deleted.js index 74eb87e..f720123 100644 --- a/src/deleted.js +++ b/src/deleted.js @@ -1,4 +1,4 @@ -import { isEmpty, isObject, hasOwnProperty } from './utils.js'; +import { isEmpty, isObject, hasOwnProperty, makeObjectWithoutPrototype } from './utils.js'; const deletedDiff = (lhs, rhs) => { if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {}; @@ -18,7 +18,7 @@ const deletedDiff = (lhs, rhs) => { acc[key] = undefined; return acc; - }, {}); + }, makeObjectWithoutPrototype()); }; export default deletedDiff; diff --git a/src/diff.js b/src/diff.js index dc7353c..bcb6f3d 100644 --- a/src/diff.js +++ b/src/diff.js @@ -1,4 +1,4 @@ -import { isDate, isEmptyObject, isObject, hasOwnProperty } from './utils.js'; +import { isDate, isEmptyObject, isObject, hasOwnProperty, makeObjectWithoutPrototype } from './utils.js'; const diff = (lhs, rhs) => { if (lhs === rhs) return {}; // equal return no diff @@ -15,7 +15,7 @@ const diff = (lhs, rhs) => { } return acc; - }, {}); + }, makeObjectWithoutPrototype()); if (isDate(l) || isDate(r)) { if (l.valueOf() == r.valueOf()) return {}; diff --git a/src/updated.js b/src/updated.js index ce81249..579d9e4 100644 --- a/src/updated.js +++ b/src/updated.js @@ -1,4 +1,4 @@ -import { isDate, isEmptyObject, isObject, hasOwnProperty } from './utils.js'; +import { isDate, isEmptyObject, isObject, hasOwnProperty, makeObjectWithoutPrototype } from './utils.js'; const updatedDiff = (lhs, rhs) => { if (lhs === rhs) return {}; @@ -26,7 +26,7 @@ const updatedDiff = (lhs, rhs) => { } return acc; - }, {}); + }, makeObjectWithoutPrototype()); }; export default updatedDiff; diff --git a/src/utils.js b/src/utils.js index 282c5d1..b56aec3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,3 +3,4 @@ export const isEmpty = o => Object.keys(o).length === 0; export const isObject = o => o != null && typeof o === 'object'; export const hasOwnProperty = (o, ...args) => Object.prototype.hasOwnProperty.call(o, ...args) export const isEmptyObject = (o) => isObject(o) && isEmpty(o); +export const makeObjectWithoutPrototype = () => Object.create(null); diff --git a/test/pollution.test.js b/test/pollution.test.js index 882400f..4307d74 100644 --- a/test/pollution.test.js +++ b/test/pollution.test.js @@ -1,85 +1,70 @@ import addedDiff from "../src/added"; +import updatedDiff from "../src/updated"; +import diff from "../src/diff"; +import deletedDiff from "../src/deleted"; describe("Prototype pollution", () => { - test("Demonstrate prototype pollution globally across all objects", () => { - const a = {}; - const b = new Object(); - - expect(a.hello).toBeUndefined(); - expect(b.hello).toBeUndefined(); - expect({}.hello).toBeUndefined(); - - b.__proto__.hello = "world"; - - expect(a.hello).toBe("world"); - expect(b.hello).toBe("world"); - expect({}.hello).toBe("world"); + describe("diff", () => { + test("should not pollute returned diffs prototype", () => { + const l = { role: "user" }; + const r = JSON.parse('{ "role": "user", "__proto__": { "role": "admin" } }'); + const difference = diff(l, r); + + expect(l.role).toBe("user"); + expect(r.role).toBe("user"); + expect(difference.role).toBeUndefined(); + }); + + test("should not pollute returned diffs prototype on nested diffs", () => { + const l = { about: { role: "user" } }; + const r = JSON.parse('{ "about": { "__proto__": { "role": "admin" } } }'); + const difference = addedDiff(l, r); + + expect(l.about.role).toBe("user"); + expect(r.about.role).toBeUndefined(); + expect(difference.about.role).toBeUndefined(); + }); }); - test("addedDiff does not pollute global prototype when running diff with added `__proto__` key", () => { - const a = { role: "user" }; - const b = JSON.parse('{ "__proto__": { "role": "admin" } }'); - - expect(a.role).toBe("user"); - expect(a.__proto__.role).toBeUndefined(); - expect(b.role).toBeUndefined(); - expect(b.__proto__.role).toBe("admin"); - expect({}.role).toBeUndefined(); - expect({}.__proto__role).toBeUndefined(); - - const difference = addedDiff(a, b); - - expect(a.role).toBe("user"); - expect(a.__proto__.role).toBeUndefined(); - expect(b.__proto__.role).toBe("admin"); - expect(b.role).toBeUndefined(); - expect({}.role).toBeUndefined(); - expect({}.__proto__role).toBeUndefined(); - - expect(difference).toEqual({ __proto__: { role: "admin" } }); + describe("addedDiff", () => { + test("addedDiff should not pollute returned diffs prototype", () => { + const l = { role: "user" }; + const r = JSON.parse('{ "__proto__": { "role": "admin" } }'); + const difference = addedDiff(l, r); + + expect(l.role).toBe("user"); + expect(r.role).toBeUndefined(); + expect(difference.role).toBeUndefined(); + }); + + test("should not pollute returned diffs prototype on nested diffs", () => { + const l = { about: { role: "user" } }; + const r = JSON.parse('{ "about": { "__proto__": { "role": "admin" } } }'); + const difference = addedDiff(l, r); + + expect(l.about.role).toBe("user"); + expect(r.about.role).toBeUndefined(); + expect(difference.about.role).toBeUndefined(); + }); }); - test("addedDiff does not pollute global prototype when running diff with added `__proto__` key generated from JSON.parse and mutating original left hand object", () => { - let a = { role: "user" }; - // Note: Don't trust `JSON.parse`!!! - const b = JSON.parse('{ "__proto__": { "role": "admin" } }'); - - expect(a.role).toBe("user"); - expect(a.__proto__.role).toBeUndefined(); - expect(b.role).toBeUndefined(); - expect(b.__proto__.role).toBe("admin"); - expect({}.role).toBeUndefined(); - expect({}.__proto__role).toBeUndefined(); - - // Note: although this does not pollute the global proto, it does pollute the original object. (Don't mutate kids!) - a = addedDiff(a, b); + test("updatedDiff should not pollute returned diffs prototype", () => { + const l = { role: "user" }; + const r = JSON.parse('{ "role": "user", "__proto__": { "role": "admin" } }'); + const difference = updatedDiff(l, r); - expect(a.role).toBe("admin"); - expect(a.__proto__.role).toBe("admin"); - expect(b.__proto__.role).toBe("admin"); - expect(b.role).toBeUndefined(); - expect({}.role).toBeUndefined(); - expect({}.__proto__role).toBeUndefined(); + expect(l.role).toBe("user"); + expect(r.role).toBe("user"); + expect(difference.role).toBeUndefined(); }); - test("addedDiff does not pollute global prototype or original object when running diff with added `__proto__` key", () => { - let a = { role: "user" }; - const b = { __proto__: { role: "admin" } }; - - expect(a.role).toBe("user"); - expect(a.__proto__.role).toBeUndefined(); - expect(b.role).toBe("admin"); - expect(b.__proto__.role).toBe("admin"); - expect({}.role).toBeUndefined(); - expect({}.__proto__role).toBeUndefined(); - - a = addedDiff(a, b); + test("deletedDiff should not pollute returned diffs prototype", () => { + const l = { role: "user" }; + const r = JSON.parse('{ "__proto__": { "role": "admin" } }'); + const difference = deletedDiff(l, r); - expect(a.role).toBeUndefined(); - expect(a.__proto__.role).toBeUndefined(); - expect(b.role).toBe("admin"); - expect(b.__proto__.role).toBe("admin"); - expect({}.role).toBeUndefined(); - expect({}.__proto__role).toBeUndefined(); + expect(l.role).toBe("user"); + expect(r.role).toBeUndefined(); + expect(difference.role).toBeUndefined(); }); });