Add prototype pollution guard to resolveConfigPath (#59694)

* config: block prototype keys in config path resolver

* config: guard config defaults fallback
This commit is contained in:
Zhang
2026-06-29 04:48:24 +08:00
committed by GitHub
parent 26f92a4f8d
commit f37e45ecc1
2 changed files with 35 additions and 1 deletions
+19
View File
@@ -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");
+16 -1
View File
@@ -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<string, unknown>)[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<string, boolean>,
): 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);