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
This commit is contained in:
Sliverp
2026-06-02 15:00:39 +08:00
committed by GitHub
parent f37ce4ed9b
commit 0552ec899f
2 changed files with 43 additions and 1 deletions

View File

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

View File

@@ -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: {