From 8cb7c9db9af1af52bb39472ca12520da5c470f40 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sun, 9 Aug 2026 00:58:49 +0900 Subject: [PATCH] fix(oauth): require verified proxy before live credential update --- src/oauth/login-cli.ts | 4 +++- tests/oauth-login-cli-live-update.test.ts | 25 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/oauth/login-cli.ts b/src/oauth/login-cli.ts index dbcf755619..780400f003 100644 --- a/src/oauth/login-cli.ts +++ b/src/oauth/login-cli.ts @@ -20,7 +20,9 @@ export async function notifyRunningProxy(name: string, provider: unknown): Promi // Identity-checked runtime-port lookup: reaches a fallback-port proxy and avoids // posting credentials-adjacent config to whatever else answers on config.port. const live = await findLiveProxy(); - if (!live) return; + // A public /healthz response is only a discovery hint. Do not disclose provider + // credentials unless the reported/discovered process passed PID identity checks. + if (!live || live.pid === null) return; try { await fetch(`http://${probeHostname(live.hostname)}:${live.port}/api/providers`, { method: "POST", diff --git a/tests/oauth-login-cli-live-update.test.ts b/tests/oauth-login-cli-live-update.test.ts index 06e993e524..61cf2ef132 100644 --- a/tests/oauth-login-cli-live-update.test.ts +++ b/tests/oauth-login-cli-live-update.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadConfig, saveConfig } from "../src/config"; import { upsertOAuthProvider } from "../src/oauth"; -import { notifyRunningProxyAfterOAuthLogin } from "../src/oauth/login-cli"; +import { notifyRunningProxy, notifyRunningProxyAfterOAuthLogin } from "../src/oauth/login-cli"; import { startServer } from "../src/server"; import type { OcxConfig } from "../src/types"; import { installIsolatedCodexHome, type IsolatedCodexHome } from "./helpers/isolated-codex-home"; @@ -53,6 +53,29 @@ afterEach(() => { }); describe("CLI OAuth live-update credential preservation", () => { + test("does not send provider credentials to a spoofed configured-port listener", async () => { + const requests: Array<{ path: string; body: string }> = []; + const fake = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + async fetch(request) { + const url = new URL(request.url); + if (url.pathname === "/healthz") { + return Response.json({ status: "ok", service: "opencodex", version: "test", uptime: 1, pid: 999_999 }); + } + requests.push({ path: url.pathname, body: await request.text() }); + return Response.json({ ok: true }); + }, + }); + try { + saveConfig(keyModeXaiConfig(fake.port)); + await notifyRunningProxy("xai", { apiKey: "live-update-sentinel-key" }); + expect(requests).toEqual([]); + } finally { + await fake.stop(true); + } + }); + test("notify after OAuth login keeps key billing on live and disk configs", async () => { const server = startServer(0); try {