From 1413550110e07509ba046223be6b261333e6ec70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=E1=B4=8D=E1=B4=9C=CA=80=E1=B4=80=20Yu=CC=84?= Date: Fri, 10 Jan 2025 22:41:25 +0900 Subject: [PATCH] fix(assert): handle `__proto__` correctly in `assertObjectMatch` --- assert/object_match.ts | 57 +++++++++++++++++++++---------------- assert/object_match_test.ts | 20 +++++++++++++ deno.json | 2 +- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/assert/object_match.ts b/assert/object_match.ts index 7e138253e2db..fe869845f522 100644 --- a/assert/object_match.ts +++ b/assert/object_match.ts @@ -52,15 +52,23 @@ function isObject(val: unknown): boolean { return typeof val === "object" && val !== null; } +function defineProperty(target: object, key: PropertyKey, value: unknown) { + return Object.defineProperty(target, key, { + value, + configurable: true, + enumerable: true, + writable: true, + }); +} + function filter(a: loose, b: loose): loose { const seen = new WeakMap(); return filterObject(a, b); function filterObject(a: loose, b: loose): loose { // Prevent infinite loop with circular references with same filter - if ((seen.has(a)) && (seen.get(a) === b)) { - return a; - } + const memo = seen.get(a); + if (memo && (memo === b)) return a; try { seen.set(a, b); @@ -82,17 +90,14 @@ function filter(a: loose, b: loose): loose { if (keysA.length && keysB.length && !entries.length) { // If both objects are not empty but don't have the same keys or symbols, // returns the entries in object a. - for (const key of keysA) { - filtered[key] = a[key]; - } - + for (const key of keysA) defineProperty(filtered, key, a[key]); return filtered; } for (const [key, value] of entries) { // On regexp references, keep value as it to avoid loosing pattern and flags if (value instanceof RegExp) { - filtered[key] = value; + defineProperty(filtered, key, value); continue; } @@ -100,7 +105,7 @@ function filter(a: loose, b: loose): loose { // On array references, build a filtered array and filter nested objects inside if (Array.isArray(value) && Array.isArray(subset)) { - filtered[key] = filterArray(value, subset); + defineProperty(filtered, key, filterArray(value, subset)); continue; } @@ -108,16 +113,19 @@ function filter(a: loose, b: loose): loose { if (isObject(value) && isObject(subset)) { // When both operands are maps, build a filtered map with common keys and filter nested objects inside if ((value instanceof Map) && (subset instanceof Map)) { - filtered[key] = new Map( - [...value].filter(([k]) => subset.has(k)).map( - ([k, v]) => { - const v2 = subset.get(k); - if (isObject(v) && isObject(v2)) { - return [k, filterObject(v as loose, v2 as loose)]; - } - - return [k, v]; - }, + defineProperty( + filtered, + key, + new Map( + [...value].filter(([k]) => subset.has(k)).map( + ([k, v]) => { + const v2 = subset.get(k); + if (isObject(v) && isObject(v2)) { + return [k, filterObject(v as loose, v2 as loose)]; + } + return [k, v]; + }, + ), ), ); continue; @@ -125,15 +133,15 @@ function filter(a: loose, b: loose): loose { // When both operands are set, build a filtered set with common values if ((value instanceof Set) && (subset instanceof Set)) { - filtered[key] = value.intersection(subset); + defineProperty(filtered, key, value.intersection(subset)); continue; } - filtered[key] = filterObject(value as loose, subset as loose); + defineProperty(filtered, key, filterObject(value as loose, subset as loose)); continue; } - filtered[key] = value; + defineProperty(filtered, key, value); } return filtered; @@ -141,9 +149,8 @@ function filter(a: loose, b: loose): loose { function filterArray(a: unknown[], b: unknown[]): unknown[] { // Prevent infinite loop with circular references with same filter - if (seen.has(a) && (seen.get(a) === b)) { - return a; - } + const memo = seen.get(a); + if (memo && (memo === b)) return a; seen.set(a, b); diff --git a/assert/object_match_test.ts b/assert/object_match_test.ts index 94386ec0ea40..15d7a0c4d110 100644 --- a/assert/object_match_test.ts +++ b/assert/object_match_test.ts @@ -413,3 +413,23 @@ Deno.test("assertObjectMatch() prints inputs correctly", () => { }`, ); }); + +Deno.test( + "assertObjectMatch() should be able to test target object's own `__proto__`", + () => { + const objectA = { ["__proto__"]: { polluted: true } }; + const objectB = { ["__proto__"]: { polluted: true } }; + const objectC = { ["__proto__"]: { polluted: false } }; + assertObjectMatch(objectA, objectB) + assertThrows( + () => assertObjectMatch(objectA, objectC), + AssertionError, + ` { + ['__proto__']: { +- polluted: true, ++ polluted: false, + }, + }`, + ); + } +); diff --git a/deno.json b/deno.json index 54db28f199a4..29af17fe907b 100644 --- a/deno.json +++ b/deno.json @@ -8,7 +8,7 @@ }, "importMap": "./import_map.json", "tasks": { - "test": "deno test --unstable-http --unstable-webgpu --doc --allow-all --parallel --coverage --trace-leaks --clean", + "test": "deno test --unstable-http --unstable-webgpu --unstable-unsafe-proto --doc --allow-all --parallel --coverage --trace-leaks --clean", "test:browser": "git grep --name-only \"This module is browser compatible.\" | grep -v deno.json | grep -v .github/workflows | grep -v _tools | grep -v encoding/README.md | grep -v media_types/vendor/update.ts | xargs deno check --config browser-compat.tsconfig.json", "test:node": "(cd _tools/node_test_runner && npm install) && node --import ./_tools/node_test_runner/register_deno_shim.mjs ./_tools/node_test_runner/run_test.mjs", "fmt:licence-headers": "deno run --allow-read --allow-write ./_tools/check_licence.ts",