fix(qa-channel): reject malformed inline attachment data · openclaw/openclaw@84ec355
vincentkoc
·
2026-05-14
·
via Recent Commits to openclaw:main
File tree
extensions/qa-channel/src
| Original file line number | Diff line number | Diff line change |
|---|
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
|
38 | 38 | - Google Meet/Codex: report malformed node proxy `payloadJSON` responses with plugin-owned errors instead of leaking raw JSON parser failures. |
39 | 39 | - Debug proxy: reject malformed relative-form proxy targets with a controlled 400 response instead of letting URL parsing escape the request handler. |
40 | 40 | - File transfer: reject malformed inline `file_write` base64 before computing hashes or invoking paired nodes, avoiding Node's lenient base64 decoder. |
| 41 | +- QA channel: skip malformed inline inbound attachment base64 instead of staging silently corrupted media for agent turns. |
41 | 42 | - 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. |
42 | 43 | - 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. |
43 | 44 | - 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. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -124,6 +124,31 @@ describe("handleQaInbound", () => {
|
124 | 124 | expect(ctxPayload?.SenderId).toBe("alice"); |
125 | 125 | }); |
126 | 126 | |
| 127 | +it("skips malformed inline attachment base64 without dropping the message", async () => { |
| 128 | +const runtime = createPluginRuntimeMock(); |
| 129 | +setQaChannelRuntime(runtime); |
| 130 | + |
| 131 | +await handleQaInbound( |
| 132 | +createQaInboundParams({ |
| 133 | +message: { |
| 134 | +attachments: [ |
| 135 | +{ |
| 136 | +id: "attachment-1", |
| 137 | +kind: "image", |
| 138 | +mimeType: "image/png", |
| 139 | +contentBase64: "AAA@@@", |
| 140 | +}, |
| 141 | +], |
| 142 | +}, |
| 143 | +}), |
| 144 | +); |
| 145 | + |
| 146 | +expect(runtime.channel.turn.runAssembled).toHaveBeenCalledTimes(1); |
| 147 | +const ctxPayload = firstRunAssembledParams(runtime).ctxPayload; |
| 148 | +expect(ctxPayload.MediaPath).toBeUndefined(); |
| 149 | +expect(ctxPayload.MediaPaths).toBeUndefined(); |
| 150 | +}); |
| 151 | + |
127 | 152 | it("uses allowFrom as the group sender fallback for allowlist policy", async () => { |
128 | 153 | const runtime = createPluginRuntimeMock(); |
129 | 154 | setQaChannelRuntime(runtime); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -19,6 +19,18 @@ export function isHttpMediaUrl(value: string): boolean {
|
19 | 19 | } |
20 | 20 | } |
21 | 21 | |
| 22 | +function normalizeBase64ForCompare(value: string): string { |
| 23 | +return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/"); |
| 24 | +} |
| 25 | + |
| 26 | +function decodeAttachmentBase64(value: string): Buffer | null { |
| 27 | +const buffer = Buffer.from(value, "base64"); |
| 28 | +if (normalizeBase64ForCompare(buffer.toString("base64")) !== normalizeBase64ForCompare(value)) { |
| 29 | +return null; |
| 30 | +} |
| 31 | +return buffer; |
| 32 | +} |
| 33 | + |
22 | 34 | async function resolveQaInboundMediaPayload(attachments: QaBusMessage["attachments"]) { |
23 | 35 | if (!Array.isArray(attachments) || attachments.length === 0) { |
24 | 36 | return {}; |
@@ -29,8 +41,13 @@ async function resolveQaInboundMediaPayload(attachments: QaBusMessage["attachmen
|
29 | 41 | continue; |
30 | 42 | } |
31 | 43 | if (typeof attachment.contentBase64 === "string" && attachment.contentBase64.trim()) { |
| 44 | +const buffer = decodeAttachmentBase64(attachment.contentBase64); |
| 45 | +if (!buffer) { |
| 46 | +console.warn("[qa-channel] inbound attachment contentBase64 rejected (invalid base64)"); |
| 47 | +continue; |
| 48 | +} |
32 | 49 | const saved = await saveMediaBuffer( |
33 | | -Buffer.from(attachment.contentBase64, "base64"), |
| 50 | +buffer, |
34 | 51 | attachment.mimeType, |
35 | 52 | "inbound", |
36 | 53 | undefined, |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。