fix(qa-channel): reject malformed inline attachment data

This commit is contained in:
Vincent Koc
2026-05-14 17:54:41 +08:00
parent 23ed804657
commit 84ec355af8
3 changed files with 44 additions and 1 deletions

View File

@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
- 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.
- QA channel: skip malformed inline inbound attachment base64 instead of staging silently corrupted media for agent turns.
- 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

@@ -124,6 +124,31 @@ describe("handleQaInbound", () => {
expect(ctxPayload?.SenderId).toBe("alice");
});
it("skips malformed inline attachment base64 without dropping the message", async () => {
const runtime = createPluginRuntimeMock();
setQaChannelRuntime(runtime);
await handleQaInbound(
createQaInboundParams({
message: {
attachments: [
{
id: "attachment-1",
kind: "image",
mimeType: "image/png",
contentBase64: "AAA@@@",
},
],
},
}),
);
expect(runtime.channel.turn.runAssembled).toHaveBeenCalledTimes(1);
const ctxPayload = firstRunAssembledParams(runtime).ctxPayload;
expect(ctxPayload.MediaPath).toBeUndefined();
expect(ctxPayload.MediaPaths).toBeUndefined();
});
it("uses allowFrom as the group sender fallback for allowlist policy", async () => {
const runtime = createPluginRuntimeMock();
setQaChannelRuntime(runtime);

View File

@@ -19,6 +19,18 @@ export function isHttpMediaUrl(value: string): boolean {
}
}
function normalizeBase64ForCompare(value: string): string {
return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
}
function decodeAttachmentBase64(value: string): Buffer | null {
const buffer = Buffer.from(value, "base64");
if (normalizeBase64ForCompare(buffer.toString("base64")) !== normalizeBase64ForCompare(value)) {
return null;
}
return buffer;
}
async function resolveQaInboundMediaPayload(attachments: QaBusMessage["attachments"]) {
if (!Array.isArray(attachments) || attachments.length === 0) {
return {};
@@ -29,8 +41,13 @@ async function resolveQaInboundMediaPayload(attachments: QaBusMessage["attachmen
continue;
}
if (typeof attachment.contentBase64 === "string" && attachment.contentBase64.trim()) {
const buffer = decodeAttachmentBase64(attachment.contentBase64);
if (!buffer) {
console.warn("[qa-channel] inbound attachment contentBase64 rejected (invalid base64)");
continue;
}
const saved = await saveMediaBuffer(
Buffer.from(attachment.contentBase64, "base64"),
buffer,
attachment.mimeType,
"inbound",
undefined,