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 <noreply@anthropic.com>
This commit is contained in:
Facu_Viñas 2026-03-11 17:59:32 -03:00
parent 41f362841e
commit c67151bef3
2 changed files with 21 additions and 1 deletions

View file

@ -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);

View file

@ -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;
}