fix(file-transfer): validate inline write base64

This commit is contained in:
Vincent Koc
2026-05-14 17:51:20 +08:00
parent 92524fcf98
commit 23cfc81bcd
3 changed files with 43 additions and 1 deletions

View File

@@ -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.

View File

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

View File

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