fix(security): OC-53 validate prompt size before string concatenation to prevent memory exhaustion — Aether AI Agent

This commit is contained in:
Aether AI Agent
2026-02-18 15:48:08 +11:00
committed by Peter Steinberger
parent 732e53151e
commit ebcf19746f
2 changed files with 23 additions and 13 deletions

View File

@@ -6,25 +6,33 @@ export type GatewayAttachment = {
content: string;
};
export function extractTextFromPrompt(prompt: ContentBlock[]): string {
export function extractTextFromPrompt(prompt: ContentBlock[], maxBytes?: number): string {
const parts: string[] = [];
// Track accumulated byte count per block to catch oversized prompts before full concatenation
let totalBytes = 0;
for (const block of prompt) {
let blockText: string | undefined;
if (block.type === "text") {
parts.push(block.text);
continue;
}
if (block.type === "resource") {
blockText = block.text;
} else if (block.type === "resource") {
const resource = block.resource as { text?: string } | undefined;
if (resource?.text) {
parts.push(resource.text);
blockText = resource.text;
}
continue;
}
if (block.type === "resource_link") {
} else if (block.type === "resource_link") {
const title = block.title ? ` (${block.title})` : "";
const uri = block.uri ?? "";
const line = uri ? `[Resource link${title}] ${uri}` : `[Resource link${title}]`;
parts.push(line);
blockText = uri ? `[Resource link${title}] ${uri}` : `[Resource link${title}]`;
}
if (blockText !== undefined) {
// Guard: reject before allocating the full concatenated string
if (maxBytes !== undefined) {
totalBytes += Buffer.byteLength(blockText, "utf-8");
if (totalBytes > maxBytes) {
throw new Error(`Prompt exceeds maximum allowed size of ${maxBytes} bytes`);
}
}
parts.push(blockText);
}
}
return parts.join("\n");

View File

@@ -264,13 +264,15 @@ export class AcpGatewayAgent implements Agent {
this.sessionStore.setActiveRun(params.sessionId, runId, abortController);
const meta = parseSessionMeta(params._meta);
const userText = extractTextFromPrompt(params.prompt);
// Pass MAX_PROMPT_BYTES so extractTextFromPrompt rejects oversized content
// block-by-block, before the full string is ever assembled in memory (CWE-400)
const userText = extractTextFromPrompt(params.prompt, MAX_PROMPT_BYTES);
const attachments = extractAttachmentsFromPrompt(params.prompt);
const prefixCwd = meta.prefixCwd ?? this.opts.prefixCwd ?? true;
const displayCwd = shortenHomePath(session.cwd);
const message = prefixCwd ? `[Working directory: ${displayCwd}]\n\n${userText}` : userText;
// Guard against oversized prompts that could cause memory exhaustion (DoS)
// Defense-in-depth: also check the final assembled message (includes cwd prefix)
if (Buffer.byteLength(message, "utf-8") > MAX_PROMPT_BYTES) {
throw new Error(
`Prompt exceeds maximum allowed size of ${MAX_PROMPT_BYTES} bytes`,