OpenClaw: ACP prompt-size checks missing in local stdio bridge could reduce responsiveness with very large inputs
Description
OpenClaw is a personal AI assistant. In versions 2026.2.17 and below, the ACP bridge accepts very large prompt text blocks and can assemble oversized prompt payloads before forwarding them to chat.send. Because ACP runs over local stdio, this mainly affects local ACP clients (for example IDE integrations) that send unusually large inputs. This issue has been fixed in version 2026.2.19.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.2.19 | 2026.2.19 |
Affected products
1Patches
38ae2d5110f6cfix(docker): pin base images to SHA256 digests (#7734)
11 files changed · +83 −9
Dockerfile+1 −1 modified@@ -1,4 +1,4 @@ -FROM node:22-bookworm +FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 # Install Bun (required for build scripts) RUN curl -fsSL https://bun.sh/install | bash
Dockerfile.sandbox+1 −1 modified@@ -1,4 +1,4 @@ -FROM debian:bookworm-slim +FROM debian:bookworm-slim@sha256:98f4b71de414932439ac6ac690d7060df1f27161073c5036a7553723881bffbe ENV DEBIAN_FRONTEND=noninteractive
Dockerfile.sandbox-browser+1 −1 modified@@ -1,4 +1,4 @@ -FROM debian:bookworm-slim +FROM debian:bookworm-slim@sha256:98f4b71de414932439ac6ac690d7060df1f27161073c5036a7553723881bffbe ENV DEBIAN_FRONTEND=noninteractive
.github/dependabot.yml+13 −0 modified@@ -111,3 +111,16 @@ updates: - minor - patch open-pull-requests-limit: 5 + + # Docker base images + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + docker-images: + patterns: + - "*" + open-pull-requests-limit: 5
scripts/docker/cleanup-smoke/Dockerfile+1 −1 modified@@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim +FROM node:22-bookworm-slim@sha256:3cfe526ec8dd62013b8843e8e5d4877e297b886e5aace4a59fec25dc20736e45 RUN apt-get update \ && apt-get install -y --no-install-recommends \
scripts/docker/install-sh-e2e/Dockerfile+1 −1 modified@@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim +FROM node:22-bookworm-slim@sha256:3cfe526ec8dd62013b8843e8e5d4877e297b886e5aace4a59fec25dc20736e45 RUN apt-get update \ && apt-get install -y --no-install-recommends \
scripts/docker/install-sh-nonroot/Dockerfile+1 −1 modified@@ -1,4 +1,4 @@ -FROM ubuntu:24.04 +FROM ubuntu:24.04@sha256:cd1dba651b3080c3686ecf4e3c4220f026b521fb76978881737d24f200828b2b RUN set -eux; \ for attempt in 1 2 3; do \
scripts/docker/install-sh-smoke/Dockerfile+1 −1 modified@@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim +FROM node:22-bookworm-slim@sha256:3cfe526ec8dd62013b8843e8e5d4877e297b886e5aace4a59fec25dc20736e45 RUN set -eux; \ for attempt in 1 2 3; do \
scripts/e2e/Dockerfile+1 −1 modified@@ -1,4 +1,4 @@ -FROM node:22-bookworm +FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 RUN corepack enable
scripts/e2e/Dockerfile.qr-import+1 −1 modified@@ -1,4 +1,4 @@ -FROM node:22-bookworm +FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 RUN corepack enable
src/docker-image-digests.test.ts+61 −0 added@@ -0,0 +1,61 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; + +const repoRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), ".."); + +const DIGEST_PINNED_DOCKERFILES = [ + "Dockerfile", + "Dockerfile.sandbox", + "Dockerfile.sandbox-browser", + "scripts/docker/cleanup-smoke/Dockerfile", + "scripts/docker/install-sh-e2e/Dockerfile", + "scripts/docker/install-sh-nonroot/Dockerfile", + "scripts/docker/install-sh-smoke/Dockerfile", + "scripts/e2e/Dockerfile", + "scripts/e2e/Dockerfile.qr-import", +] as const; + +type DependabotDockerGroup = { + patterns?: string[]; +}; + +type DependabotUpdate = { + "package-ecosystem"?: string; + directory?: string; + schedule?: { interval?: string }; + groups?: Record<string, DependabotDockerGroup>; +}; + +type DependabotConfig = { + updates?: DependabotUpdate[]; +}; + +describe("docker base image pinning", () => { + it("pins selected Dockerfile FROM lines to immutable sha256 digests", async () => { + for (const dockerfilePath of DIGEST_PINNED_DOCKERFILES) { + const dockerfile = await readFile(resolve(repoRoot, dockerfilePath), "utf8"); + const fromLine = dockerfile + .split(/\r?\n/) + .find((line) => line.trimStart().startsWith("FROM ")); + expect(fromLine, `${dockerfilePath} should define a FROM line`).toBeDefined(); + expect(fromLine, `${dockerfilePath} FROM must be digest-pinned`).toMatch( + /^FROM\s+\S+@sha256:[a-f0-9]{64}$/, + ); + } + }); + + it("keeps Dependabot Docker updates enabled for root Dockerfiles", async () => { + const raw = await readFile(resolve(repoRoot, ".github/dependabot.yml"), "utf8"); + const config = parse(raw) as DependabotConfig; + const dockerUpdate = config.updates?.find( + (update) => update["package-ecosystem"] === "docker" && update.directory === "/", + ); + + expect(dockerUpdate).toBeDefined(); + expect(dockerUpdate?.schedule?.interval).toBe("weekly"); + expect(dockerUpdate?.groups?.["docker-images"]?.patterns).toContain("*"); + }); +});
63e39d7f57acfix(security): harden ACP prompt size guardrails
5 files changed · +89 −10
CHANGELOG.md+1 −0 modified@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Security/Plugins/Hooks: enforce runtime/package path containment with realpath checks so `openclaw.extensions`, `openclaw.hooks`, and hook handler modules cannot escape their trusted roots via traversal or symlinks. - Security/Discord: centralize trusted sender checks for moderation actions in message-action dispatch, share moderation command parsing across handlers, and clarify permission helpers with explicit any/all semantics. - Security/ACP: harden ACP bridge session management with duplicate-session refresh, idle-session reaping, oldest-idle soft-cap eviction, and burst rate limiting on session creation to reduce local DoS risk without disrupting normal IDE usage. +- Security/ACP: bound ACP prompt text payloads to 2 MiB before gateway forwarding, account for join separator bytes during pre-concatenation size checks, and avoid stale active-run session state when oversized prompts are rejected. Thanks @aether-ai-agent for reporting. - Security/Plugins/Hooks: add optional `--pin` for npm plugin/hook installs, persist resolved npm metadata (`name`, `version`, `spec`, integrity, shasum, timestamp), warn/confirm on integrity drift during updates, and extend `openclaw security audit` to flag unpinned specs, missing integrity metadata, and install-record version drift. - Security/Plugins: harden plugin discovery by blocking unsafe candidates (root escapes, world-writable paths, suspicious ownership), add startup warnings when `plugins.allow` is empty with discoverable non-bundled plugins, and warn on loaded plugins without install/load-path provenance. - Refactor/Plugins: extract shared plugin path-safety utilities, split discovery safety checks into typed reasoned guards, precompute provenance matchers during plugin load, and switch ownership tests to injected uid inputs.
src/acp/client.test.ts+22 −0 modified@@ -153,6 +153,28 @@ describe("acp event mapper", () => { expect(text).toBe("Hello\nFile contents\n[Resource link (Spec)] https://example.com"); }); + it("counts newline separators toward prompt byte limits", () => { + expect(() => + extractTextFromPrompt( + [ + { type: "text", text: "a" }, + { type: "text", text: "b" }, + ], + 2, + ), + ).toThrow(/maximum allowed size/i); + + expect( + extractTextFromPrompt( + [ + { type: "text", text: "a" }, + { type: "text", text: "b" }, + ], + 3, + ), + ).toBe("a\nb"); + }); + it("extracts image blocks into gateway attachments", () => { const attachments = extractAttachmentsFromPrompt([ { type: "image", data: "abc", mimeType: "image/png" },
src/acp/event-mapper.ts+2 −1 modified@@ -27,7 +27,8 @@ export function extractTextFromPrompt(prompt: ContentBlock[], maxBytes?: number) if (blockText !== undefined) { // Guard: reject before allocating the full concatenated string if (maxBytes !== undefined) { - totalBytes += Buffer.byteLength(blockText, "utf-8"); + const separatorBytes = parts.length > 0 ? 1 : 0; // "\n" added by join() between blocks + totalBytes += separatorBytes + Buffer.byteLength(blockText, "utf-8"); if (totalBytes > maxBytes) { throw new Error(`Prompt exceeds maximum allowed size of ${maxBytes} bytes`); }
src/acp/translator.session-rate-limit.test.ts+59 −2 modified@@ -2,6 +2,7 @@ import type { AgentSideConnection, LoadSessionRequest, NewSessionRequest, + PromptRequest, } from "@agentclientprotocol/sdk"; import { describe, expect, it, vi } from "vitest"; import type { GatewayClient } from "../gateway/client.js"; @@ -14,9 +15,11 @@ function createConnection(): AgentSideConnection { } as unknown as AgentSideConnection; } -function createGateway(): GatewayClient { +function createGateway( + request: GatewayClient["request"] = vi.fn(async () => ({ ok: true })) as GatewayClient["request"], +): GatewayClient { return { - request: vi.fn(async () => ({ ok: true })), + request, } as unknown as GatewayClient; } @@ -37,6 +40,18 @@ function createLoadSessionRequest(sessionId: string, cwd = "/tmp"): LoadSessionR } as unknown as LoadSessionRequest; } +function createPromptRequest( + sessionId: string, + text: string, + meta: Record<string, unknown> = {}, +): PromptRequest { + return { + sessionId, + prompt: [{ type: "text", text }], + _meta: meta, + } as unknown as PromptRequest; +} + describe("acp session creation rate limit", () => { it("rate limits excessive newSession bursts", async () => { const sessionStore = createInMemorySessionStore(); @@ -76,3 +91,45 @@ describe("acp session creation rate limit", () => { sessionStore.clearAllSessionsForTest(); }); }); + +describe("acp prompt size hardening", () => { + it("rejects oversized prompt blocks without leaking active runs", async () => { + const request = vi.fn(async () => ({ ok: true })); + const sessionStore = createInMemorySessionStore(); + const agent = new AcpGatewayAgent(createConnection(), createGateway(request), { + sessionStore, + }); + const sessionId = "prompt-limit-oversize"; + await agent.loadSession(createLoadSessionRequest(sessionId)); + + await expect( + agent.prompt(createPromptRequest(sessionId, "a".repeat(2 * 1024 * 1024 + 1))), + ).rejects.toThrow(/maximum allowed size/i); + expect(request).not.toHaveBeenCalledWith("chat.send", expect.anything(), expect.anything()); + const session = sessionStore.getSession(sessionId); + expect(session?.activeRunId).toBeNull(); + expect(session?.abortController).toBeNull(); + + sessionStore.clearAllSessionsForTest(); + }); + + it("rejects oversize final messages from cwd prefix without leaking active runs", async () => { + const request = vi.fn(async () => ({ ok: true })); + const sessionStore = createInMemorySessionStore(); + const agent = new AcpGatewayAgent(createConnection(), createGateway(request), { + sessionStore, + }); + const sessionId = "prompt-limit-prefix"; + await agent.loadSession(createLoadSessionRequest(sessionId)); + + await expect( + agent.prompt(createPromptRequest(sessionId, "a".repeat(2 * 1024 * 1024))), + ).rejects.toThrow(/maximum allowed size/i); + expect(request).not.toHaveBeenCalledWith("chat.send", expect.anything(), expect.anything()); + const session = sessionStore.getSession(sessionId); + expect(session?.activeRunId).toBeNull(); + expect(session?.abortController).toBeNull(); + + sessionStore.clearAllSessionsForTest(); + }); +});
src/acp/translator.ts+5 −7 modified@@ -259,10 +259,6 @@ export class AcpGatewayAgent implements Agent { this.sessionStore.cancelActiveRun(params.sessionId); } - const abortController = new AbortController(); - const runId = randomUUID(); - this.sessionStore.setActiveRun(params.sessionId, runId, abortController); - const meta = parseSessionMeta(params._meta); // Pass MAX_PROMPT_BYTES so extractTextFromPrompt rejects oversized content // block-by-block, before the full string is ever assembled in memory (CWE-400) @@ -274,11 +270,13 @@ export class AcpGatewayAgent implements Agent { // 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`, - ); + throw new Error(`Prompt exceeds maximum allowed size of ${MAX_PROMPT_BYTES} bytes`); } + const abortController = new AbortController(); + const runId = randomUUID(); + this.sessionStore.setActiveRun(params.sessionId, runId, abortController); + return new Promise<PromptResponse>((resolve, reject) => { this.pendingPrompts.set(params.sessionId, { sessionId: params.sessionId,
ebcf19746f5cfix(security): OC-53 validate prompt size before string concatenation to prevent memory exhaustion — Aether AI Agent
2 files changed · +23 −13
src/acp/event-mapper.ts+19 −11 modified@@ -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");
src/acp/translator.ts+4 −2 modified@@ -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`,
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-cxpw-2g23-2vgwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27576ghsaADVISORY
- github.com/openclaw/openclaw/commit/63e39d7f57ac4ad4a5e38d17e7394ae7c4dd0b9cghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/commit/8ae2d5110f6ceadef73822aa3db194fb60d2ba68ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/commit/ebcf19746f5c500a41817e03abecadea8655654aghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/releases/tag/v2026.2.19ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/security/advisories/GHSA-cxpw-2g23-2vgwghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.