Skip to content

Commit 0552ec8

Browse files
authored
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
1 parent f37ce4e commit 0552ec8

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

extensions/qqbot/src/engine/api/token.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ describe("QQBot token manager", () => {
4242
url: "https://bots.qq.com/app/getAppAccessToken",
4343
auditContext: "qqbot-token",
4444
capture: false,
45+
policy: {
46+
hostnameAllowlist: ["bots.qq.com"],
47+
allowRfc2544BenchmarkRange: true,
48+
},
4549
init: {
4650
method: "POST",
4751
headers: {
@@ -54,6 +58,25 @@ describe("QQBot token manager", () => {
5458
expect(release).toHaveBeenCalledTimes(1);
5559
});
5660

61+
it("passes the RFC2544 SSRF allowance to the token fetch (regression for #88984)", async () => {
62+
mockGuardedTokenResponse('{"access_token":"token-1","expires_in":7200}', {
63+
status: 200,
64+
headers: { "content-type": "application/json" },
65+
});
66+
67+
await expect(new TokenManager().getAccessToken("app-id", "secret")).resolves.toBe("token-1");
68+
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
69+
expect.objectContaining({
70+
url: "https://bots.qq.com/app/getAppAccessToken",
71+
auditContext: "qqbot-token",
72+
policy: {
73+
hostnameAllowlist: ["bots.qq.com"],
74+
allowRfc2544BenchmarkRange: true,
75+
},
76+
}),
77+
);
78+
});
79+
5780
it("does not cache access tokens forever when expires_in is unsafe", async () => {
5881
vi.useFakeTimers();
5982
vi.setSystemTime(new Date("2026-05-29T12:00:00.000Z"));

extensions/qqbot/src/engine/api/token.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,31 @@ import {
1212
resolveExpiresAtMsFromDurationSeconds,
1313
resolveTimestampMsToIsoString,
1414
} from "openclaw/plugin-sdk/number-runtime";
15-
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
15+
import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
1616
import type { EngineLogger } from "../types.js";
1717
import { formatErrorMessage } from "../utils/format.js";
1818

1919
const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
2020
const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 7200;
2121

22+
/**
23+
* Host-scoped SSRF policy for the QQ Bot token endpoint.
24+
*
25+
* `TOKEN_URL` is a hard-coded `https://bots.qq.com/...` constant, so this
26+
* relaxation only ever applies to that single host. Fake-IP proxy stacks
27+
* (sing-box, Clash, Surge, WSL2 DNS, etc.) routinely map `bots.qq.com` into
28+
* the RFC 2544 benchmark range `198.18.0.0/15`, which the default SSRF
29+
* guard blocks. We mirror the existing media-path pattern
30+
* (`QQBOT_MEDIA_SSRF_POLICY` in `../utils/file-utils.ts`) so the relaxation
31+
* stays narrowly host-scoped instead of weakening the global default.
32+
*
33+
* See https://github.com/openclaw/openclaw/issues/88984.
34+
*/
35+
const QQBOT_TOKEN_SSRF_POLICY: SsrFPolicy = {
36+
hostnameAllowlist: ["bots.qq.com"],
37+
allowRfc2544BenchmarkRange: true,
38+
};
39+
2240
interface CachedToken {
2341
token: string;
2442
expiresAt: number;
@@ -234,6 +252,7 @@ export class TokenManager {
234252
url: TOKEN_URL,
235253
auditContext: "qqbot-token",
236254
capture: false,
255+
policy: QQBOT_TOKEN_SSRF_POLICY,
237256
init: {
238257
method: "POST",
239258
headers: {

0 commit comments

Comments
 (0)