From 0552ec899f38b6be4eae0f60f29fe5c18992e5f8 Mon Sep 17 00:00:00 2001 From: Sliverp <38134380+sliverp@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:00:39 +0800 Subject: [PATCH] fix(qqbot): allow RFC2544 benchmark range for token fetch (#88984) (#89015) * fix(qqbot): allow RFC2544 benchmark range for token fetch (#88984) QQ Bot `bots.qq.com` token-fetch path was failing for users whose DNS resolver maps the hostname into the RFC 2544 benchmark range `198.18.0.0/15` (commonly seen with fake-IP proxy stacks: sing-box, Clash, Surge, WSL2 DNS). The default SSRF guard treats that range as private and blocks the request, surfacing as "Network error getting access_token: Blocked: resolves to private/internal/special-use IP address". Pass a host-scoped `SsrFPolicy` (`allowRfc2544BenchmarkRange: true`) to the single hard-coded `TOKEN_URL` request, mirroring the existing `QQBOT_MEDIA_SSRF_POLICY` pattern used by the media path. Because `TOKEN_URL` is a const and not user-controlled, the relaxation cannot widen attack surface to other hosts. Adds a regression test asserting `policy: { allowRfc2544BenchmarkRange: true }` is forwarded into `fetchWithSsrFGuard`, and updates the existing equality assertion accordingly. Fixes #88984 * fix(qqbot): scope token ssrf policy --- extensions/qqbot/src/engine/api/token.test.ts | 23 +++++++++++++++++++ extensions/qqbot/src/engine/api/token.ts | 21 ++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/extensions/qqbot/src/engine/api/token.test.ts b/extensions/qqbot/src/engine/api/token.test.ts index 2f88f2f1b76..7389fa4c760 100644 --- a/extensions/qqbot/src/engine/api/token.test.ts +++ b/extensions/qqbot/src/engine/api/token.test.ts @@ -42,6 +42,10 @@ describe("QQBot token manager", () => { url: "https://bots.qq.com/app/getAppAccessToken", auditContext: "qqbot-token", capture: false, + policy: { + hostnameAllowlist: ["bots.qq.com"], + allowRfc2544BenchmarkRange: true, + }, init: { method: "POST", headers: { @@ -54,6 +58,25 @@ describe("QQBot token manager", () => { expect(release).toHaveBeenCalledTimes(1); }); + it("passes the RFC2544 SSRF allowance to the token fetch (regression for #88984)", async () => { + mockGuardedTokenResponse('{"access_token":"token-1","expires_in":7200}', { + status: 200, + headers: { "content-type": "application/json" }, + }); + + await expect(new TokenManager().getAccessToken("app-id", "secret")).resolves.toBe("token-1"); + expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith( + expect.objectContaining({ + url: "https://bots.qq.com/app/getAppAccessToken", + auditContext: "qqbot-token", + policy: { + hostnameAllowlist: ["bots.qq.com"], + allowRfc2544BenchmarkRange: true, + }, + }), + ); + }); + it("does not cache access tokens forever when expires_in is unsafe", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-29T12:00:00.000Z")); diff --git a/extensions/qqbot/src/engine/api/token.ts b/extensions/qqbot/src/engine/api/token.ts index 299da9a0b4f..05325452522 100644 --- a/extensions/qqbot/src/engine/api/token.ts +++ b/extensions/qqbot/src/engine/api/token.ts @@ -12,13 +12,31 @@ import { resolveExpiresAtMsFromDurationSeconds, resolveTimestampMsToIsoString, } from "openclaw/plugin-sdk/number-runtime"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; +import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; import type { EngineLogger } from "../types.js"; import { formatErrorMessage } from "../utils/format.js"; const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken"; const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 7200; +/** + * Host-scoped SSRF policy for the QQ Bot token endpoint. + * + * `TOKEN_URL` is a hard-coded `https://bots.qq.com/...` constant, so this + * relaxation only ever applies to that single host. Fake-IP proxy stacks + * (sing-box, Clash, Surge, WSL2 DNS, etc.) routinely map `bots.qq.com` into + * the RFC 2544 benchmark range `198.18.0.0/15`, which the default SSRF + * guard blocks. We mirror the existing media-path pattern + * (`QQBOT_MEDIA_SSRF_POLICY` in `../utils/file-utils.ts`) so the relaxation + * stays narrowly host-scoped instead of weakening the global default. + * + * See https://github.com/openclaw/openclaw/issues/88984. + */ +const QQBOT_TOKEN_SSRF_POLICY: SsrFPolicy = { + hostnameAllowlist: ["bots.qq.com"], + allowRfc2544BenchmarkRange: true, +}; + interface CachedToken { token: string; expiresAt: number; @@ -234,6 +252,7 @@ export class TokenManager { url: TOKEN_URL, auditContext: "qqbot-token", capture: false, + policy: QQBOT_TOKEN_SSRF_POLICY, init: { method: "POST", headers: {