From 15ff8619d128a85ba4fd98a7f6b9c2de47c48da0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 23 Apr 2026 12:13:06 -0700 Subject: [PATCH] fix(qa-channel): reject non-http attachment urls --- CHANGELOG.md | 1 + extensions/qa-channel/src/inbound.test.ts | 12 ++++++++++++ extensions/qa-channel/src/inbound.ts | 15 +++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 extensions/qa-channel/src/inbound.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ac97a93c323..b2de4ff6635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- QA channel/security: reject non-HTTP(S) inbound attachment URLs before media fetch, and log rejected schemes so suspicious or misconfigured payloads are visible during debugging. (#70708) Thanks @vincentkoc. - Teams/security: require shared Bot Framework audience tokens to name the configured Teams app via verified `appid` or `azp`, blocking cross-bot token replay on the global audience. (#70724) Thanks @vincentkoc. - Plugins/startup: resolve bundled plugin Jiti loads relative to the target plugin module instead of the central loader, so Bun global installs no longer hang while discovering bundled image providers. (#70073) Thanks @yidianyiko. - Anthropic/CLI security: stop Claude CLI backend defaults from forcing `bypassPermissions`, and strip malformed permission-mode overrides instead of silently falling back to a bypass. (#70723) Thanks @vincentkoc. diff --git a/extensions/qa-channel/src/inbound.test.ts b/extensions/qa-channel/src/inbound.test.ts new file mode 100644 index 00000000000..eb8badfd6b2 --- /dev/null +++ b/extensions/qa-channel/src/inbound.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { isHttpMediaUrl } from "./inbound.js"; + +describe("isHttpMediaUrl", () => { + it("accepts only http and https urls", () => { + expect(isHttpMediaUrl("https://example.com/image.png")).toBe(true); + expect(isHttpMediaUrl("http://example.com/image.png")).toBe(true); + expect(isHttpMediaUrl("file:///etc/passwd")).toBe(false); + expect(isHttpMediaUrl("/etc/passwd")).toBe(false); + expect(isHttpMediaUrl("data:text/plain;base64,SGVsbG8=")).toBe(false); + }); +}); diff --git a/extensions/qa-channel/src/inbound.ts b/extensions/qa-channel/src/inbound.ts index deb7b2fe43e..8a9fc929d71 100644 --- a/extensions/qa-channel/src/inbound.ts +++ b/extensions/qa-channel/src/inbound.ts @@ -9,6 +9,15 @@ import { buildQaTarget, sendQaBusMessage, type QaBusMessage } from "./bus-client import { getQaChannelRuntime } from "./runtime.js"; import type { CoreConfig, ResolvedQaChannelAccount } from "./types.js"; +export function isHttpMediaUrl(value: string): boolean { + try { + const parsed = new URL(value); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } +} + async function resolveQaInboundMediaPayload(attachments: QaBusMessage["attachments"]) { if (!Array.isArray(attachments) || attachments.length === 0) { return {}; @@ -33,6 +42,12 @@ async function resolveQaInboundMediaPayload(attachments: QaBusMessage["attachmen continue; } if (typeof attachment.url === "string" && attachment.url.trim()) { + if (!isHttpMediaUrl(attachment.url)) { + console.warn( + `[qa-channel] inbound attachment URL rejected (non-http scheme): ${attachment.url}`, + ); + continue; + } const saved = await saveMediaSource(attachment.url, undefined, "inbound"); mediaList.push({ path: saved.path,