Medium severity5.8NVD Advisory· Published May 6, 2026· Updated May 7, 2026
CVE-2026-44117
CVE-2026-44117
Description
OpenClaw before 2026.4.20 contains a server-side request forgery vulnerability in QQBot direct media upload that skips URL validation. Attackers can bypass SSRF protections by sending crafted image URLs to uploadC2CMedia and uploadGroupMedia endpoints to relay unintended requests.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.20 | 2026.4.20 |
Affected products
2Patches
149db424c8001fix(qqbot): add SSRF guard to direct-upload URL paths in uploadC2CMedia and uploadGroupMedia [AI-assisted] (#69595)
3 files changed · +168 −3
CHANGELOG.md+1 −0 modified@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- fix(qqbot): add SSRF guard to direct-upload URL paths in uploadC2CMedia and uploadGroupMedia [AI-assisted]. (#69595) Thanks @pgondhi987. - fix(gateway): enforce allowRequestSessionKey gate on template-rendered mapping sessionKeys. (#69381) Thanks @pgondhi987. - Webchat/images: treat inline image attachments as media for empty-turn gating while still ignoring metadata-only blank turns. (#69474) Thanks @Jaswir. - OpenAI/Responses: resolve `/think` levels against each GPT model's supported reasoning efforts so `/think off` no longer becomes high reasoning or sends unsupported `reasoning.effort: "none"` payloads.
extensions/qqbot/src/api.security.test.ts+145 −0 added@@ -0,0 +1,145 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const ssrfMocks = vi.hoisted(() => ({ + fetchWithSsrFGuard: vi.fn(), + resolvePinnedHostnameWithPolicy: vi.fn(), +})); + +vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ + fetchWithSsrFGuard: ssrfMocks.fetchWithSsrFGuard, + resolvePinnedHostnameWithPolicy: ssrfMocks.resolvePinnedHostnameWithPolicy, +})); + +vi.mock("./utils/debug-log.js", () => ({ + debugError: vi.fn(), + debugLog: vi.fn(), +})); + +import { MediaFileType, uploadC2CMedia, uploadGroupMedia } from "./api.js"; +import { clearUploadCache, computeFileHash, setCachedFileInfo } from "./utils/upload-cache.js"; + +describe("qqbot direct upload SSRF guard", () => { + beforeEach(() => { + vi.clearAllMocks(); + clearUploadCache(); + ssrfMocks.resolvePinnedHostnameWithPolicy.mockResolvedValue({ + hostname: "example.com", + addresses: ["203.0.113.10"], + lookup: vi.fn(), + }); + ssrfMocks.fetchWithSsrFGuard.mockResolvedValue({ + response: new Response(JSON.stringify({ file_uuid: "uuid", file_info: "info", ttl: 3600 }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + release: async () => {}, + }); + }); + + it("blocks direct-upload URLs that target private or internal hosts", async () => { + ssrfMocks.resolvePinnedHostnameWithPolicy.mockRejectedValueOnce( + new Error("Blocked hostname or private/internal/special-use IP address"), + ); + + await expect( + uploadC2CMedia( + "access-token", + "user-1", + MediaFileType.IMAGE, + "https://169.254.169.254/latest/meta-data/iam/security-credentials/", + ), + ).rejects.toThrow("Blocked hostname or private/internal/special-use IP address"); + + expect(ssrfMocks.fetchWithSsrFGuard).not.toHaveBeenCalled(); + }); + + it("blocks non-HTTPS direct-upload URLs before the QQ upload request", async () => { + await expect( + uploadGroupMedia( + "access-token", + "group-1", + MediaFileType.FILE, + "http://cdn.qpic.cn/payload.txt", + ), + ).rejects.toThrow("Direct-upload media URL must use HTTPS"); + + expect(ssrfMocks.resolvePinnedHostnameWithPolicy).not.toHaveBeenCalled(); + expect(ssrfMocks.fetchWithSsrFGuard).not.toHaveBeenCalled(); + }); + + it("allows public HTTPS direct-upload URLs", async () => { + const result = await uploadC2CMedia( + "access-token", + "user-1", + MediaFileType.IMAGE, + "https://example.com/payload.png", + ); + + expect(result).toEqual({ file_uuid: "uuid", file_info: "info", ttl: 3600 }); + expect(ssrfMocks.resolvePinnedHostnameWithPolicy).toHaveBeenCalledWith("example.com"); + expect(ssrfMocks.fetchWithSsrFGuard).toHaveBeenCalledTimes(1); + }); + + it("allows public HTTPS direct-upload URLs for group uploads", async () => { + const result = await uploadGroupMedia( + "access-token", + "group-1", + MediaFileType.FILE, + "https://example.com/payload.txt", + ); + + expect(result).toEqual({ file_uuid: "uuid", file_info: "info", ttl: 3600 }); + expect(ssrfMocks.resolvePinnedHostnameWithPolicy).toHaveBeenCalledWith("example.com"); + expect(ssrfMocks.fetchWithSsrFGuard).toHaveBeenCalledTimes(1); + }); + + it("skips URL validation on c2c cache hits when fileData is reused", async () => { + const fileData = "cached-file-data"; + setCachedFileInfo( + computeFileHash(fileData), + "c2c", + "user-1", + MediaFileType.IMAGE, + "cached-info", + "cached-uuid", + 3600, + ); + + const result = await uploadC2CMedia( + "access-token", + "user-1", + MediaFileType.IMAGE, + "https://example.com/stale.png", + fileData, + ); + + expect(result).toEqual({ file_uuid: "", file_info: "cached-info", ttl: 0 }); + expect(ssrfMocks.resolvePinnedHostnameWithPolicy).not.toHaveBeenCalled(); + expect(ssrfMocks.fetchWithSsrFGuard).not.toHaveBeenCalled(); + }); + + it("skips URL validation on group cache hits when fileData is reused", async () => { + const fileData = "cached-group-file-data"; + setCachedFileInfo( + computeFileHash(fileData), + "group", + "group-1", + MediaFileType.FILE, + "cached-group-info", + "cached-group-uuid", + 3600, + ); + + const result = await uploadGroupMedia( + "access-token", + "group-1", + MediaFileType.FILE, + "https://example.com/stale.txt", + fileData, + ); + + expect(result).toEqual({ file_uuid: "", file_info: "cached-group-info", ttl: 0 }); + expect(ssrfMocks.resolvePinnedHostnameWithPolicy).not.toHaveBeenCalled(); + expect(ssrfMocks.fetchWithSsrFGuard).not.toHaveBeenCalled(); + }); +});
extensions/qqbot/src/api.ts+22 −3 modified@@ -2,7 +2,10 @@ import { createRequire } from "node:module"; import os from "node:os"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { readPluginPackageVersion } from "openclaw/plugin-sdk/extension-shared"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; +import { + fetchWithSsrFGuard, + resolvePinnedHostnameWithPolicy, +} from "openclaw/plugin-sdk/ssrf-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { debugLog, debugError } from "./utils/debug-log.js"; import { sanitizeFileName } from "./utils/platform.js"; @@ -534,6 +537,22 @@ export interface UploadMediaResponse { id?: string; } +async function assertDirectUploadUrlAllowed(url: string): Promise<string> { + let parsed: URL; + try { + parsed = new URL(url); + } catch (err) { + throw new Error(`Invalid media URL: ${formatErrorMessage(err)}`, { cause: err }); + } + + if (parsed.protocol !== "https:") { + throw new Error("Direct-upload media URL must use HTTPS"); + } + + await resolvePinnedHostnameWithPolicy(parsed.hostname); + return parsed.toString(); +} + export async function uploadC2CMedia( accessToken: string, openid: string, @@ -557,7 +576,7 @@ export async function uploadC2CMedia( const body: Record<string, unknown> = { file_type: fileType, srv_send_msg: srvSendMsg }; if (url) { - body.url = url; + body.url = await assertDirectUploadUrlAllowed(url); } else if (fileData) { body.file_data = fileData; } @@ -610,7 +629,7 @@ export async function uploadGroupMedia( const body: Record<string, unknown> = { file_type: fileType, srv_send_msg: srvSendMsg }; if (url) { - body.url = url; + body.url = await assertDirectUploadUrlAllowed(url); } else if (fileData) { body.file_data = fileData; }
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
5- github.com/openclaw/openclaw/commit/49db424c8001f2f419aad85f434894d8d85c1a09nvdPatchWEB
- github.com/advisories/GHSA-c4qg-j8jg-42q5ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-c4qg-j8jg-42q5nvdMitigationVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-44117ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-server-side-request-forgery-in-qqbot-direct-media-uploadnvdThird Party AdvisoryWEB
News mentions
9- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- 27th April – Threat Intelligence ReportCheck Point Research · Apr 27, 2026
- Agents that remember: introducing Agent MemoryCloudflare Blog · Apr 17, 2026
- The Increasing Role of AI in Vulnerability ResearchWordfence Blog · Apr 10, 2026
- 16th March – Threat Intelligence ReportCheck Point Research · Mar 16, 2026
- How AI Assistants are Moving the Security GoalpostsKrebs on Security · Mar 8, 2026
- Risky Business #827 -- Iranian cyber threat actors are down but not outRisky Business · Mar 4, 2026
- Risky Business #826 -- A week of AI mishaps and skulduggeryRisky Business · Feb 25, 2026