From c67151bef3ea0cdc47bd58111ccb6a759604f1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facu=5FVi=C3=B1as?= Date: Wed, 11 Mar 2026 17:59:32 -0300 Subject: [PATCH] fix(security): cap user_note at 500 chars to prevent LLM context DoS Arbitrary-length free-text replies from remote channels were passed directly into the LLM context. Now truncated to 500 chars with trailing ellipsis. Co-Authored-By: Claude Opus 4.6 --- .../extensions/gsd/tests/remote-questions.test.ts | 15 +++++++++++++++ .../extensions/remote-questions/format.ts | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/tests/remote-questions.test.ts b/src/resources/extensions/gsd/tests/remote-questions.test.ts index bf37c2771..4175f1d38 100644 --- a/src/resources/extensions/gsd/tests/remote-questions.test.ts +++ b/src/resources/extensions/gsd/tests/remote-questions.test.ts @@ -87,6 +87,21 @@ test("parseDiscordResponse rejects multi-question reaction parsing", () => { assert.match(String(result.answers.second.user_note), /single-question prompts/i); }); +test("parseSlackReply truncates user_note longer than 500 chars", () => { + const longText = "x".repeat(600); + const result = parseSlackReply(longText, [{ + id: "q1", + header: "Q1", + question: "Pick", + allowMultiple: false, + options: [{ label: "A", description: "a" }], + }]); + + const note = result.answers.q1.user_note!; + assert.ok(note.length <= 502, `note should be truncated, got ${note.length} chars`); + assert.ok(note.endsWith("…"), "truncated note should end with ellipsis"); +}); + test("isValidChannelId rejects invalid Slack channel IDs", () => { // Too short assert.equal(isValidChannelId("slack", "C123"), false); diff --git a/src/resources/extensions/remote-questions/format.ts b/src/resources/extensions/remote-questions/format.ts index dd01039b8..1e03c637b 100644 --- a/src/resources/extensions/remote-questions/format.ts +++ b/src/resources/extensions/remote-questions/format.ts @@ -19,6 +19,7 @@ export interface DiscordEmbed { } const NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"]; +const MAX_USER_NOTE_LENGTH = 500; export function formatForSlack(prompt: RemotePrompt): SlackBlock[] { const blocks: SlackBlock[] = [ @@ -154,5 +155,9 @@ function parseAnswerForQuestion(text: string, q: RemoteQuestion): { answers: str return { answers: [q.options[single - 1].label] }; } - return { answers: [], user_note: text }; + return { answers: [], user_note: truncateNote(text) }; +} + +function truncateNote(text: string): string { + return text.length > MAX_USER_NOTE_LENGTH ? text.slice(0, MAX_USER_NOTE_LENGTH) + "…" : text; }