diff --git a/CHANGELOG.md b/CHANGELOG.md index f82cf606f75..7ecf12602fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - Gateway HTTP: match models, session kill, and session history route paths without trusting malformed Host headers, avoiding pre-auth 500s on those endpoints. - Google Meet/Codex: report malformed node proxy `payloadJSON` responses with plugin-owned errors instead of leaking raw JSON parser failures. - Debug proxy: reject malformed relative-form proxy targets with a controlled 400 response instead of letting URL parsing escape the request handler. +- File transfer: reject malformed inline `file_write` base64 before computing hashes or invoking paired nodes, avoiding Node's lenient base64 decoder. - Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom. - Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd. - CLI/onboarding: forward provider-specific auth flags (e.g. `--openai-api-key`) through the onboarding wizard so they reach provider auth methods via `ctx.opts`, letting `--openai-api-key "$OPENAI_API_KEY"` skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf. diff --git a/extensions/file-transfer/src/tools/file-write-tool.test.ts b/extensions/file-transfer/src/tools/file-write-tool.test.ts new file mode 100644 index 00000000000..e6b47bb01b5 --- /dev/null +++ b/extensions/file-transfer/src/tools/file-write-tool.test.ts @@ -0,0 +1,29 @@ +import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime"; +import { describe, expect, it, vi } from "vitest"; +import { createFileWriteTool } from "./file-write-tool.js"; + +vi.mock("openclaw/plugin-sdk/agent-harness-runtime", () => ({ + callGatewayTool: vi.fn(), + listNodes: vi.fn(), + resolveNodeIdFromList: vi.fn(), +})); + +vi.mock("openclaw/plugin-sdk/media-store", () => ({ + readMediaBuffer: vi.fn(), +})); + +describe("file_write tool", () => { + it("rejects malformed inline base64 before invoking the node", async () => { + const tool = createFileWriteTool(); + + await expect( + tool.execute("tool-call-1", { + node: "node-1", + path: "/tmp/out.txt", + contentBase64: "AAA@@@", + }), + ).rejects.toThrow("contentBase64 is not valid base64"); + + expect(callGatewayTool).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/file-transfer/src/tools/file-write-tool.ts b/extensions/file-transfer/src/tools/file-write-tool.ts index e900cdb430c..2ae3260bdd6 100644 --- a/extensions/file-transfer/src/tools/file-write-tool.ts +++ b/extensions/file-transfer/src/tools/file-write-tool.ts @@ -21,6 +21,18 @@ import { FILE_WRITE_TOOL_DESCRIPTOR, } from "./descriptors.js"; +function normalizeBase64ForCompare(value: string): string { + return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/"); +} + +function decodeStrictBase64(value: string): Buffer { + const buffer = Buffer.from(value, "base64"); + if (normalizeBase64ForCompare(buffer.toString("base64")) !== normalizeBase64ForCompare(value)) { + throw new Error("contentBase64 is not valid base64"); + } + return buffer; +} + async function readSourceBytes(input: { contentBase64?: string; sourceMediaId?: string; @@ -37,7 +49,7 @@ async function readSourceBytes(input: { if (input.contentBase64 === undefined) { throw new Error("contentBase64 or sourceMediaId required"); } - const buffer = Buffer.from(input.contentBase64, "base64"); + const buffer = decodeStrictBase64(input.contentBase64); return { buffer, contentBase64: input.contentBase64, source: "inline" }; }