fix(openai): allow fake-IP OAuth token exchange (#110096)

Keep the OpenAI OAuth token endpoint usable behind fake-IP proxy DNS while retaining exact-host SSRF protection for redirects and unrelated hosts.

Co-authored-by: Abner Shang <75654486+abnershang@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-07-17 23:14:15 +01:00
committed by GitHub
co-authored by Abner Shang
parent 79c7569fa6
commit 84ff010eba
2 changed files with 87 additions and 4 deletions
@@ -7,10 +7,21 @@ const ssrfMocks = vi.hoisted(() => ({
fetchWithSsrFGuard: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
fetchWithSsrFGuard: ssrfMocks.fetchWithSsrFGuard,
}));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/ssrf-runtime")>(
"openclaw/plugin-sdk/ssrf-runtime",
);
return {
...actual,
fetchWithSsrFGuard: ssrfMocks.fetchWithSsrFGuard,
};
});
import {
resolvePinnedHostnameWithPolicy,
type LookupFn,
type SsrFPolicy,
} from "openclaw/plugin-sdk/ssrf-runtime";
import {
createOpenAIAuthorizationFlow,
resolveOpenAICallbackHost,
@@ -65,6 +76,36 @@ function mockTokenResponseText(body: string, status = 200): void {
});
}
function mockFakeIpTokenResponse(params: { address: string; family: 4 | 6 }): void {
ssrfMocks.fetchWithSsrFGuard.mockImplementationOnce(
async ({ policy }: { policy?: SsrFPolicy }) => {
const lookupFn = vi.fn(async () => [params]) as unknown as LookupFn;
const pinned = await resolvePinnedHostnameWithPolicy("auth.openai.com", {
lookupFn,
policy,
});
expect(pinned.addresses).toEqual([params.address]);
await expect(
resolvePinnedHostnameWithPolicy("redirect.example.com", { lookupFn, policy }),
).rejects.toThrow("Blocked hostname (not in allowlist)");
expect(lookupFn).toHaveBeenCalledOnce();
return {
response: new Response(
JSON.stringify({
access_token: "test-access-token",
refresh_token: "test-refresh-token",
expires_in: 3600,
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
release: vi.fn(async () => undefined),
};
},
);
}
afterEach(() => {
ssrfMocks.fetchWithSsrFGuard.mockReset();
vi.unstubAllGlobals();
@@ -230,6 +271,40 @@ describe("OpenAI Codex OAuth flow", () => {
});
});
it.each([
{ operation: "authorization-code exchange", address: "198.18.0.42", family: 4 as const },
{ operation: "refresh-token exchange", address: "fc00::42", family: 6 as const },
])(
"allows fake-IP DNS for the OpenAI OAuth $operation",
async ({ operation, address, family }) => {
mockFakeIpTokenResponse({ address, family });
const result =
operation === "authorization-code exchange"
? await exchangeOpenAIAuthorizationCode(
"code",
"verifier",
resolveOpenAIRedirectUri("localhost"),
)
: await refreshOpenAIAccessToken("old-refresh-token");
expect(result).toMatchObject({
type: "success",
access: "test-access-token",
refresh: "test-refresh-token",
});
expect(ssrfMocks.fetchWithSsrFGuard).toHaveBeenCalledWith(
expect.objectContaining({
policy: {
allowRfc2544BenchmarkRange: true,
allowIpv6UniqueLocalRange: true,
hostnameAllowlist: ["auth.openai.com"],
},
}),
);
},
);
it("cancels token exchange requests with the caller signal", async () => {
const controller = new AbortController();
controller.abort();
@@ -3,11 +3,16 @@ import {
resolveOAuthTokenLifetimeMs,
} from "openclaw/plugin-sdk/provider-oauth-runtime";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
import { throwIfOAuthLoginAborted } from "./openai-chatgpt-oauth-abort.runtime.js";
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
const TOKEN_URL = "https://auth.openai.com/oauth/token";
const OAUTH_TOKEN_SSRF_POLICY = {
allowRfc2544BenchmarkRange: true,
allowIpv6UniqueLocalRange: true,
hostnameAllowlist: ["auth.openai.com"],
} satisfies SsrFPolicy;
const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
const OAUTH_TOKEN_RESPONSE_BODY_LIMIT_BYTES = 1 * 1024 * 1024;
@@ -61,6 +66,9 @@ async function postTokenForm(
throwIfOAuthLoginAborted(options.signal);
const { response, release } = await fetchWithSsrFGuard({
url: TOKEN_URL,
// Fake-IP proxies map public hosts into these ranges. The exact-host allowlist
// keeps redirects and every other hostname fail-closed.
policy: OAUTH_TOKEN_SSRF_POLICY,
init: {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },