From fe8d99d42129f529d35ace4cc1fb6a12667af0cf Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 23 May 2026 22:03:18 -0700 Subject: [PATCH] fix(security): escape field names in transcript regex extraction extractJsonStringFieldPrefix and extractJsonNullableStringFieldPrefix interpolate the `field` parameter into `new RegExp(...)` without escaping. All current callers pass hardcoded strings ("id", "parentId", "type", "role"), but the function signature accepts any string. A future caller passing a field containing regex metacharacters (e.g. "foo.bar") would match unintended patterns. Wrap the interpolation with escapeRegExp() from src/shared/regexp.ts so metacharacters are treated literally. Signed-off-by: Sebastien Tardif --- src/gateway/session-utils.fs.test.ts | 33 ++++++++++++++++++++++++++++ src/gateway/session-utils.fs.ts | 5 +++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index b3351deab73c..b18e144cef0e 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -1982,6 +1982,39 @@ describe("oversized transcript line guards", () => { expectUsageFields(usage, { modelProvider: "test-provider" }); }); + test("oversized line metadata extraction preserves id and parentId", async () => { + const sessionId = "test-oversized-metadata-extract"; + const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`); + const oversizedContent = "w".repeat(300 * 1024); + const lines = [ + JSON.stringify({ type: "session", version: 3, id: sessionId }), + JSON.stringify({ + type: "message", + id: "root-msg", + parentId: null, + message: { role: "user", content: "root" }, + }), + JSON.stringify({ + type: "message", + id: "oversized-child", + parentId: "root-msg", + message: { role: "assistant", content: oversizedContent }, + }), + ]; + fs.writeFileSync(transcriptPath, `${lines.join("\n")}\n`, "utf-8"); + + const out = await readRecentSessionMessagesAsync(sessionId, storePath, undefined, { + maxMessages: 10, + }); + + const serialized = JSON.stringify(out); + // The oversized line's id and parentId must be extracted correctly + // from the prefix via regex-based field extraction. + expect(serialized).toContain("oversized-child"); + expect(serialized).toContain("root-msg"); + expect(serialized).not.toContain(oversizedContent); + }); + test("readSessionTitleFieldsFromTranscriptAsync delegates to bounded sync reader", async () => { const sessionId = "test-async-title-bounded"; writeTranscript( diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index d9eac5f6546d..c58f99f3ebe9 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -4,6 +4,7 @@ import { deriveSessionTotalTokens, hasNonzeroUsage, normalizeUsage } from "../ag import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js"; import { hasInterSessionUserProvenance } from "../sessions/input-provenance.js"; import { extractAssistantVisibleText } from "../shared/chat-message-content.js"; +import { escapeRegExp } from "../shared/regexp.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { estimateStringChars, estimateTokensFromChars } from "../utils/cjk-chars.js"; import { stripInlineDirectiveTagsForDisplay } from "../utils/directive-tags.js"; @@ -273,7 +274,7 @@ function isOversizedTranscriptLine(line: string): boolean { } function extractJsonStringFieldPrefix(prefix: string, field: string): string | undefined { - const match = new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`).exec(prefix); + const match = new RegExp(`"${escapeRegExp(field)}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`).exec(prefix); if (!match) { return undefined; } @@ -289,7 +290,7 @@ function extractJsonNullableStringFieldPrefix( prefix: string, field: string, ): string | null | undefined { - if (new RegExp(`"${field}"\\s*:\\s*null`).test(prefix)) { + if (new RegExp(`"${escapeRegExp(field)}"\\s*:\\s*null`).test(prefix)) { return null; } return extractJsonStringFieldPrefix(prefix, field);