fix(qa-channel): reject non-http attachment urls

This commit is contained in:
Vincent Koc
2026-04-23 12:13:06 -07:00
parent 44144cbd06
commit 15ff8619d1
3 changed files with 28 additions and 0 deletions

View File

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

View File

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

View File

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