Skip to content

Commit

Permalink
Disable prototype pollution on returned diff object
Browse files Browse the repository at this point in the history
  • Loading branch information
mattphillips committed Nov 12, 2022
1 parent ba84464 commit 9576963
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 80 deletions.
4 changes: 2 additions & 2 deletions src/added.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEmpty, isObject, hasOwnProperty } from './utils.js';
import { isEmpty, isObject, hasOwnProperty, makeObjectWithoutPrototype } from './utils.js';

const addedDiff = (lhs, rhs) => {

Expand All @@ -19,7 +19,7 @@ const addedDiff = (lhs, rhs) => {

acc[key] = r[key];
return acc;
}, {});
}, makeObjectWithoutPrototype());
};

export default addedDiff;
4 changes: 2 additions & 2 deletions src/deleted.js
Original file line number Diff line number Diff line change
@@ -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 {};
Expand All @@ -18,7 +18,7 @@ const deletedDiff = (lhs, rhs) => {

acc[key] = undefined;
return acc;
}, {});
}, makeObjectWithoutPrototype());
};

export default deletedDiff;
4 changes: 2 additions & 2 deletions src/diff.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,7 +15,7 @@ const diff = (lhs, rhs) => {
}

return acc;
}, {});
}, makeObjectWithoutPrototype());

if (isDate(l) || isDate(r)) {
if (l.valueOf() == r.valueOf()) return {};
Expand Down
4 changes: 2 additions & 2 deletions src/updated.js
Original file line number Diff line number Diff line change
@@ -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 {};
Expand Down Expand Up @@ -26,7 +26,7 @@ const updatedDiff = (lhs, rhs) => {
}

return acc;
}, {});
}, makeObjectWithoutPrototype());
};

export default updatedDiff;
1 change: 1 addition & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
129 changes: 57 additions & 72 deletions test/pollution.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});

0 comments on commit 9576963

Please sign in to comment.