fix(security): scope OAuth callback postMessage targets and re-enable TLS verification on DNS-bypass fetch (#998)

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 <aeon@aeonframework.dev>
This commit is contained in:
@aaronjmars 2026-05-10 10:10:48 -04:00 committed by GitHub
parent c7c1074f28
commit 52c38cf94c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 23 additions and 8 deletions

View file

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

View file

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