Skip to content

Commit e526054

Browse files
zenglingbiaovincentkoc
authored andcommitted
fix(config): restrict config paths to own properties
1 parent d71a24f commit e526054

2 files changed

Lines changed: 55 additions & 7 deletions

File tree

src/config/config-paths.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
getConfigValueAtPath,
4+
setConfigValueAtPath,
5+
unsetConfigValueAtPath,
6+
} from "./config-paths.js";
7+
8+
describe("config path own-property traversal", () => {
9+
for (const key of ["toString", "valueOf", "hasOwnProperty"]) {
10+
it(`does not treat inherited ${key} as config`, () => {
11+
const parent: Record<string, unknown> = {};
12+
const root: Record<string, unknown> = { parent };
13+
14+
expect(getConfigValueAtPath(root, ["parent", key])).toBeUndefined();
15+
expect(unsetConfigValueAtPath(root, ["parent", key])).toBe(false);
16+
expect(root).toEqual({ parent: {} });
17+
18+
setConfigValueAtPath(root, ["parent", key], "own");
19+
expect(Object.hasOwn(parent, key)).toBe(true);
20+
expect(getConfigValueAtPath(root, ["parent", key])).toBe("own");
21+
expect(unsetConfigValueAtPath(root, ["parent", key])).toBe(true);
22+
expect(root).toEqual({});
23+
});
24+
}
25+
26+
it("replaces an inherited parent instead of traversing it", () => {
27+
const prototypeBranch = { leaf: "prototype" };
28+
const root = Object.create({ branch: prototypeBranch }) as Record<string, unknown>;
29+
30+
expect(getConfigValueAtPath(root, ["branch", "leaf"])).toBeUndefined();
31+
expect(unsetConfigValueAtPath(root, ["branch", "leaf"])).toBe(false);
32+
expect(prototypeBranch).toEqual({ leaf: "prototype" });
33+
34+
setConfigValueAtPath(root, ["branch", "leaf"], "own");
35+
expect(Object.hasOwn(root, "branch")).toBe(true);
36+
expect(root.branch).toEqual({ leaf: "own" });
37+
expect(prototypeBranch).toEqual({ leaf: "prototype" });
38+
39+
expect(unsetConfigValueAtPath(root, ["branch", "leaf"])).toBe(true);
40+
expect(Object.hasOwn(root, "branch")).toBe(false);
41+
expect(getConfigValueAtPath(root, ["branch", "leaf"])).toBeUndefined();
42+
expect(prototypeBranch).toEqual({ leaf: "prototype" });
43+
});
44+
});

src/config/config-paths.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
12
// Resolves and classifies config paths for reads, writes, and metadata.
23
import { isPlainObject } from "../utils.js";
3-
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
44

55
type PathNode = Record<string, unknown>;
66

@@ -37,11 +37,12 @@ export function setConfigValueAtPath(root: PathNode, path: string[], value: unkn
3737
let cursor: PathNode = root;
3838
for (let idx = 0; idx < path.length - 1; idx += 1) {
3939
const key = path[idx];
40-
const next = cursor[key];
41-
if (!isPlainObject(next)) {
42-
cursor[key] = {};
40+
const existing = Object.hasOwn(cursor, key) ? cursor[key] : undefined;
41+
const next: PathNode = isPlainObject(existing) ? existing : {};
42+
if (next !== existing) {
43+
cursor[key] = next;
4344
}
44-
cursor = cursor[key] as PathNode;
45+
cursor = next;
4546
}
4647
cursor[path[path.length - 1]] = value;
4748
}
@@ -52,6 +53,9 @@ export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean
5253
let cursor: PathNode = root;
5354
for (let idx = 0; idx < path.length - 1; idx += 1) {
5455
const key = path[idx];
56+
if (!Object.hasOwn(cursor, key)) {
57+
return false;
58+
}
5559
const next = cursor[key];
5660
if (!isPlainObject(next)) {
5761
return false;
@@ -60,7 +64,7 @@ export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean
6064
cursor = next;
6165
}
6266
const leafKey = path[path.length - 1];
63-
if (!(leafKey in cursor)) {
67+
if (!Object.hasOwn(cursor, leafKey)) {
6468
return false;
6569
}
6670
delete cursor[leafKey];
@@ -82,7 +86,7 @@ export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean
8286
export function getConfigValueAtPath(root: PathNode, path: string[]): unknown {
8387
let cursor: unknown = root;
8488
for (const key of path) {
85-
if (!isPlainObject(cursor)) {
89+
if (!isPlainObject(cursor) || !Object.hasOwn(cursor, key)) {
8690
return undefined;
8791
}
8892
cursor = cursor[key];

0 commit comments

Comments
 (0)