Skip to content

Commit 0c4d187

Browse files
committed
Config: bound schema lookup paths
1 parent 5fc2d0f commit 0c4d187

File tree

4 files changed

+55
-3
lines changed

4 files changed

+55
-3
lines changed

src/config/schema.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,31 @@ describe("config schema", () => {
271271
expect(lookupConfigSchema(baseSchema, "__proto__.polluted")).toBeNull();
272272
});
273273

274+
it("rejects overly deep lookup paths", () => {
275+
const buildNestedObjectSchema = (segments: string[]) => {
276+
const [head, ...rest] = segments;
277+
if (!head) {
278+
return { type: "string" };
279+
}
280+
return {
281+
type: "object",
282+
properties: {
283+
[head]: buildNestedObjectSchema(rest),
284+
},
285+
};
286+
};
287+
288+
const deepPathSegments = Array.from({ length: 33 }, (_, index) => `a${index}`);
289+
const deepSchema = {
290+
schema: buildNestedObjectSchema(deepPathSegments),
291+
uiHints: {},
292+
version: "test",
293+
generatedAt: "test",
294+
} as unknown as Parameters<typeof lookupConfigSchema>[0];
295+
296+
expect(lookupConfigSchema(deepSchema, deepPathSegments.join("."))).toBeNull();
297+
});
298+
274299
it("returns null for missing config schema paths", () => {
275300
expect(lookupConfigSchema(baseSchema, "gateway.notReal.path")).toBeNull();
276301
});

src/config/schema.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const LOOKUP_SCHEMA_BOOLEAN_KEYS = new Set([
5151
"readOnly",
5252
"writeOnly",
5353
]);
54+
const MAX_LOOKUP_PATH_SEGMENTS = 32;
5455

5556
function cloneSchema<T>(value: T): T {
5657
if (typeof structuredClone === "function") {
@@ -682,12 +683,16 @@ export function lookupConfigSchema(
682683
if (!normalizedPath) {
683684
return null;
684685
}
686+
const parts = splitLookupPath(normalizedPath);
687+
if (parts.length === 0 || parts.length > MAX_LOOKUP_PATH_SEGMENTS) {
688+
return null;
689+
}
685690

686691
let current = asSchemaObject(response.schema);
687692
if (!current) {
688693
return null;
689694
}
690-
for (const segment of splitLookupPath(normalizedPath)) {
695+
for (const segment of parts) {
691696
const next = resolveLookupChildSchema(current, segment);
692697
if (!next) {
693698
return null;

src/gateway/protocol/schema/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Type } from "@sinclair/typebox";
22
import { NonEmptyString } from "./primitives.js";
33

4-
const NonWhitespaceString = Type.String({ minLength: 1, pattern: ".*\\S.*" });
4+
const ConfigSchemaLookupPathString = Type.String({
5+
minLength: 1,
6+
maxLength: 1024,
7+
pattern: "^[A-Za-z0-9_.\\[\\]\\-*]+$",
8+
});
59

610
export const ConfigGetParamsSchema = Type.Object({}, { additionalProperties: false });
711

@@ -31,7 +35,7 @@ export const ConfigSchemaParamsSchema = Type.Object({}, { additionalProperties:
3135

3236
export const ConfigSchemaLookupParamsSchema = Type.Object(
3337
{
34-
path: NonWhitespaceString,
38+
path: ConfigSchemaLookupPathString,
3539
},
3640
{ additionalProperties: false },
3741
);

src/gateway/server.config-patch.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,24 @@ describe("gateway config methods", () => {
8787
expect(res.error?.message ?? "").toContain("invalid config.schema.lookup params");
8888
});
8989

90+
it("rejects config.schema.lookup when the path exceeds the protocol limit", async () => {
91+
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.schema.lookup", {
92+
path: `gateway.${"a".repeat(1020)}`,
93+
});
94+
95+
expect(res.ok).toBe(false);
96+
expect(res.error?.message ?? "").toContain("invalid config.schema.lookup params");
97+
});
98+
99+
it("rejects config.schema.lookup when the path contains invalid characters", async () => {
100+
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.schema.lookup", {
101+
path: "gateway.auth\nspoof",
102+
});
103+
104+
expect(res.ok).toBe(false);
105+
expect(res.error?.message ?? "").toContain("invalid config.schema.lookup params");
106+
});
107+
90108
it("rejects prototype-chain config.schema.lookup paths without reflecting them", async () => {
91109
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.schema.lookup", {
92110
path: "constructor",

0 commit comments

Comments
 (0)