From 4e791759cfecaa74319959fe800e0a34c680c442 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 17 Feb 2026 21:32:30 -0800 Subject: [PATCH] Address second-pass PR feedback on parser hardening Co-authored-by: codex --- apps/server/src/keybindings.test.ts | 8 +++++ apps/server/src/keybindings.ts | 47 +++++++++++++++++++---------- apps/web/src/projectScripts.ts | 1 + 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/apps/server/src/keybindings.test.ts b/apps/server/src/keybindings.test.ts index 66d90a519e877dab66263f232fcb7a68002d82f7..3bd533da95c329ee0d7a29370a2341a38efd920c 100644 --- a/apps/server/src/keybindings.test.ts +++ b/apps/server/src/keybindings.test.ts @@ -91,6 +91,14 @@ describe("server keybindings", () => { when: "terminalFocus && (", }), ); + + assert.isNull( + compileResolvedKeybindingRule({ + key: "mod+d", + command: "terminal.split", + when: `${"!".repeat(300)}terminalFocus`, + }), + ); }); it("upserts custom keybindings to ~/.t3/keybindings.json", () => { diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index 5b1a1b4738694cd4421a5c59a4116b2cb29f23a4..edbb08794c519cb03ad1ce4840ad966e4776ddc9 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -24,6 +24,8 @@ type WhenToken = | { type: "lparen" } | { type: "rparen" }; +const MAX_WHEN_EXPRESSION_DEPTH = 256; + export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, @@ -159,7 +161,10 @@ export function parseKeybindingWhenExpression(expression: string): KeybindingWhe if (!tokens || tokens.length === 0) return null; let index = 0; - const parsePrimary = (): KeybindingWhenNode | null => { + const parsePrimary = (depth: number): KeybindingWhenNode | null => { + if (depth > MAX_WHEN_EXPRESSION_DEPTH) { + return null; + } const token = tokens[index]; if (!token) return null; @@ -170,7 +175,7 @@ export function parseKeybindingWhenExpression(expression: string): KeybindingWhe if (token.type === "lparen") { index += 1; - const expressionNode = parseOr(); + const expressionNode = parseOr(depth + 1); const closeToken = tokens[index]; if (!expressionNode || !closeToken || closeToken.type !== "rparen") { return null; @@ -182,24 +187,34 @@ export function parseKeybindingWhenExpression(expression: string): KeybindingWhe return null; }; - const parseUnary = (): KeybindingWhenNode | null => { - const token = tokens[index]; - if (token?.type === "not") { + const parseUnary = (depth: number): KeybindingWhenNode | null => { + let notCount = 0; + while (tokens[index]?.type === "not") { index += 1; - const node = parseUnary(); - if (!node) return null; - return { type: "not", node }; + notCount += 1; + if (notCount > MAX_WHEN_EXPRESSION_DEPTH) { + return null; + } } - return parsePrimary(); + + let node = parsePrimary(depth); + if (!node) return null; + + while (notCount > 0) { + node = { type: "not", node }; + notCount -= 1; + } + + return node; }; - const parseAnd = (): KeybindingWhenNode | null => { - let left = parseUnary(); + const parseAnd = (depth: number): KeybindingWhenNode | null => { + let left = parseUnary(depth); if (!left) return null; while (tokens[index]?.type === "and") { index += 1; - const right = parseUnary(); + const right = parseUnary(depth); if (!right) return null; left = { type: "and", left, right }; } @@ -207,13 +222,13 @@ export function parseKeybindingWhenExpression(expression: string): KeybindingWhe return left; }; - const parseOr = (): KeybindingWhenNode | null => { - let left = parseAnd(); + const parseOr = (depth: number): KeybindingWhenNode | null => { + let left = parseAnd(depth); if (!left) return null; while (tokens[index]?.type === "or") { index += 1; - const right = parseAnd(); + const right = parseAnd(depth); if (!right) return null; left = { type: "or", left, right }; } @@ -221,7 +236,7 @@ export function parseKeybindingWhenExpression(expression: string): KeybindingWhe return left; }; - const ast = parseOr(); + const ast = parseOr(0); if (!ast || index !== tokens.length) return null; return ast; } diff --git a/apps/web/src/projectScripts.ts b/apps/web/src/projectScripts.ts index a71bd3e3be651b7d9b01598ca0435cabf5b91a71..d093138a667104a9656f0dd1735d678e3bfb2490 100644 --- a/apps/web/src/projectScripts.ts +++ b/apps/web/src/projectScripts.ts @@ -48,6 +48,7 @@ export function nextProjectScriptId(name: string, existingIds: Iterable) suffix += 1; } + // This last-resort fallback only triggers after exhausting thousands of suffixes. return `${baseId}-${Date.now()}`.slice(0, MAX_SCRIPT_ID_LENGTH); } -- 2.54.0