From 52c38cf94c9723829e718a4fff90c4eec84e0295 Mon Sep 17 00:00:00 2001 From: "@aaronjmars" <61592645+aaronjmars@users.noreply.github.com> Date: Sun, 10 May 2026 10:10:48 -0400 Subject: [PATCH] fix(security): scope OAuth callback postMessage targets and re-enable TLS verification on DNS-bypass fetch (#998) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings, neither blocked by anything else: 1. src/app/callback/page.js — the OAuth callback page posted the { code, state } payload to window.opener with targetOrigin "*", so any page that opened the popup against the well-known redirect_uri received the live OAuth code. The expectedOrigins list was already computed but never used. Iterate over it and pass the origin per send. 2. open-sse/utils/proxyFetch.js — createBypassRequest() set rejectUnauthorized: false on the HTTPS request that runs after the Google-DNS-resolved real-IP fallback (used for cloudcode-pa.googleapis, GitHub Copilot, Cursor, AWS LLM endpoints). Combined with servername: parsedUrl.hostname this gave SNI-correct connections that nonetheless ignored cert validation, so an on-path attacker could swap in their own cert and read the user's API tokens / prompts. Drop the flag. Detected by Aeon + semgrep (javascript.browser.security.wildcard-postmessage-configuration + problem-based-packs.insecure-transport.js-node.bypass-tls-verification). Severity: HIGH (#1) / MEDIUM (#2). CWEs: CWE-1385 (#1), CWE-295 (#2). Co-authored-by: aeonframework --- open-sse/utils/proxyFetch.js | 7 ++++++- src/app/callback/page.js | 24 +++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/open-sse/utils/proxyFetch.js b/open-sse/utils/proxyFetch.js index 837d29e..43b7164 100644 --- a/open-sse/utils/proxyFetch.js +++ b/open-sse/utils/proxyFetch.js @@ -155,8 +155,13 @@ async function createBypassRequest(parsedUrl, realIP, options) { socket.connect(HTTPS_PORT, realIP, () => { const reqOptions = { socket, + // SNI + cert hostname are validated against the hostname the caller + // asked for, not the IP we connected to. This keeps the DNS-bypass + // (avoiding /etc/hosts MITM) while still rejecting on-path attackers + // that present a different cert. The MITM_BYPASS_HOSTS targets are + // all public-CA-issued (Google / GitHub / AWS / Cursor) so default + // verification works without any extra trust store. servername: parsedUrl.hostname, - rejectUnauthorized: false, path: parsedUrl.pathname + parsedUrl.search, method: options.method || "POST", headers: { diff --git a/src/app/callback/page.js b/src/app/callback/page.js index 4064b03..e55370d 100644 --- a/src/app/callback/page.js +++ b/src/app/callback/page.js @@ -26,19 +26,29 @@ function CallbackContent() { let relayed = false; - // Check if this callback is from expected origin/port + // Trusted origins that may receive this callback. The OAuth code/state + // must only be relayed to the dashboard window we expect to be the opener + // (same origin) or the Codex helper that listens on a fixed loopback port. + // Any other origin is treated as hostile (drive-by attacker that opened + // the popup against the well-known redirect_uri to phish the code). const expectedOrigins = [ window.location.origin, // Same origin (for most providers) "http://localhost:1455", // Codex specific port ]; - + // Method 1: postMessage to opener (popup mode) + // Send once per expected origin. The browser delivers the message only + // when the opener's origin matches the targetOrigin we pass — using "*" + // here would leak the code/state to any opener (e.g. an attacker page + // that opened this URL in a popup), so iterate over the allowlist. if (window.opener) { - try { - window.opener.postMessage({ type: "oauth_callback", data: callbackData }, "*"); - relayed = true; - } catch (e) { - console.log("postMessage failed:", e); + for (const origin of expectedOrigins) { + try { + window.opener.postMessage({ type: "oauth_callback", data: callbackData }, origin); + relayed = true; + } catch (e) { + console.log("postMessage failed:", e); + } } }