From f37e45ecc1f4a32d6dbd3dca095e50ed17021ec8 Mon Sep 17 00:00:00 2001 From: Zhang <56248212+YonganZhang@users.noreply.github.com> Date: Mon, 29 Jun 2026 04:48:24 +0800 Subject: [PATCH] Add prototype pollution guard to resolveConfigPath (#59694) * config: block prototype keys in config path resolver * config: guard config defaults fallback --- src/shared/config-eval.test.ts | 19 +++++++++++++++++++ src/shared/config-eval.ts | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/shared/config-eval.test.ts b/src/shared/config-eval.test.ts index 429d6f2b6825..d59b8f6d51f1 100644 --- a/src/shared/config-eval.test.ts +++ b/src/shared/config-eval.test.ts @@ -59,6 +59,19 @@ describe("config-eval helpers", () => { expect(resolveConfigPath("not-an-object", "browser.enabled")).toBeUndefined(); }); + it("blocks prototype keys while resolving config paths", () => { + const config = { + safe: { + enabled: true, + }, + }; + + expect(resolveConfigPath(config, "safe.enabled")).toBe(true); + expect(resolveConfigPath(config, "__proto__")).toBeUndefined(); + expect(resolveConfigPath(config, "constructor.name")).toBeUndefined(); + expect(resolveConfigPath(config, "prototype.polluted")).toBeUndefined(); + }); + it("uses defaults only when config paths are unresolved", () => { const config = { browser: { @@ -75,6 +88,12 @@ describe("config-eval helpers", () => { expect(isConfigPathTruthyWithDefaults(config, "browser.other", {})).toBe(false); }); + it("does not use inherited defaults for blocked config paths", () => { + expect(isConfigPathTruthyWithDefaults({}, "constructor", {})).toBe(false); + expect(isConfigPathTruthyWithDefaults({}, "__proto__.enabled", {})).toBe(false); + expect(isConfigPathTruthyWithDefaults({}, "prototype.enabled", {})).toBe(false); + }); + it("returns the active runtime platform", () => { setPlatform("darwin"); expect(resolveRuntimePlatform()).toBe("darwin"); diff --git a/src/shared/config-eval.ts b/src/shared/config-eval.ts index e95c04c9783a..754b0d7030e3 100644 --- a/src/shared/config-eval.ts +++ b/src/shared/config-eval.ts @@ -1,6 +1,7 @@ // Config evaluation helpers load dynamic config modules with guarded evaluation. import fs from "node:fs"; import path from "node:path"; +import { isBlockedObjectKey } from "../infra/prototype-keys.js"; /** Normalizes primitive config values into the truthiness rules used by requirements checks. */ export function isTruthy(value: unknown): boolean { @@ -27,11 +28,21 @@ export function resolveConfigPath(config: unknown, pathStr: string): unknown { if (typeof current !== "object" || current === null) { return undefined; } + if (isBlockedObjectKey(part)) { + return undefined; + } current = (current as Record)[part]; } return current; } +function hasBlockedConfigPathSegment(pathStr: string): boolean { + return pathStr + .split(".") + .filter(Boolean) + .some((part) => isBlockedObjectKey(part)); +} + /** Checks a config path with fallback defaults only when the path is unresolved. */ export function isConfigPathTruthyWithDefaults( config: unknown, @@ -39,7 +50,11 @@ export function isConfigPathTruthyWithDefaults( defaults: Record, ): boolean { const value = resolveConfigPath(config, pathStr); - if (value === undefined && pathStr in defaults) { + if ( + value === undefined && + !hasBlockedConfigPathSegment(pathStr) && + Object.hasOwn(defaults, pathStr) + ) { return defaults[pathStr] ?? false; } return isTruthy(value);