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>
149 lines
5.3 KiB
JavaScript
149 lines
5.3 KiB
JavaScript
"use client";
|
|
|
|
import { Suspense, useEffect, useState } from "react";
|
|
import { useSearchParams } from "next/navigation";
|
|
|
|
/**
|
|
* OAuth Callback Page Content
|
|
*/
|
|
function CallbackContent() {
|
|
const searchParams = useSearchParams();
|
|
const [status, setStatus] = useState("processing");
|
|
|
|
useEffect(() => {
|
|
const code = searchParams.get("code");
|
|
const state = searchParams.get("state");
|
|
const error = searchParams.get("error");
|
|
const errorDescription = searchParams.get("error_description");
|
|
|
|
const callbackData = {
|
|
code,
|
|
state,
|
|
error,
|
|
errorDescription,
|
|
fullUrl: window.location.href,
|
|
};
|
|
|
|
let relayed = false;
|
|
|
|
// 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) {
|
|
for (const origin of expectedOrigins) {
|
|
try {
|
|
window.opener.postMessage({ type: "oauth_callback", data: callbackData }, origin);
|
|
relayed = true;
|
|
} catch (e) {
|
|
console.log("postMessage failed:", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 2: BroadcastChannel (same origin tabs)
|
|
try {
|
|
const channel = new BroadcastChannel("oauth_callback");
|
|
channel.postMessage(callbackData);
|
|
channel.close();
|
|
relayed = true;
|
|
} catch (e) {
|
|
console.log("BroadcastChannel failed:", e);
|
|
}
|
|
|
|
// Method 3: localStorage event (fallback)
|
|
try {
|
|
localStorage.setItem("oauth_callback", JSON.stringify({ ...callbackData, timestamp: Date.now() }));
|
|
relayed = true;
|
|
} catch (e) {
|
|
console.log("localStorage failed:", e);
|
|
}
|
|
|
|
if (!(code || error)) {
|
|
setTimeout(() => setStatus("manual"), 0);
|
|
return;
|
|
}
|
|
|
|
setStatus("success");
|
|
setTimeout(() => {
|
|
window.close();
|
|
setTimeout(() => setStatus("done"), 500);
|
|
}, 1500);
|
|
}, [searchParams]);
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-bg">
|
|
<div className="text-center p-8 max-w-md">
|
|
{status === "processing" && (
|
|
<>
|
|
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
|
|
<span className="material-symbols-outlined text-3xl text-primary animate-spin">progress_activity</span>
|
|
</div>
|
|
<h1 className="text-xl font-semibold mb-2">Processing...</h1>
|
|
<p className="text-text-muted">Please wait while we complete the authorization.</p>
|
|
</>
|
|
)}
|
|
|
|
{(status === "success" || status === "done") && (
|
|
<>
|
|
<div className="size-16 mx-auto mb-4 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
|
|
<span className="material-symbols-outlined text-3xl text-green-600">check_circle</span>
|
|
</div>
|
|
<h1 className="text-xl font-semibold mb-2">Authorization Successful!</h1>
|
|
<p className="text-text-muted">
|
|
{status === "success" ? "This window will close automatically..." : "You can close this tab now."}
|
|
</p>
|
|
</>
|
|
)}
|
|
|
|
{status === "manual" && (
|
|
<>
|
|
<div className="size-16 mx-auto mb-4 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
|
|
<span className="material-symbols-outlined text-3xl text-yellow-600">info</span>
|
|
</div>
|
|
<h1 className="text-xl font-semibold mb-2">Copy This URL</h1>
|
|
<p className="text-text-muted mb-4">
|
|
Please copy the URL from the address bar and paste it in the application.
|
|
</p>
|
|
<div className="bg-surface border border-border rounded-lg p-3 text-left">
|
|
<code className="text-xs break-all">{typeof window !== "undefined" ? window.location.href : ""}</code>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* OAuth Callback Page
|
|
* Receives callback from OAuth providers and sends data back via multiple methods
|
|
*/
|
|
export default function CallbackPage() {
|
|
return (
|
|
<Suspense fallback={
|
|
<div className="min-h-screen flex items-center justify-center bg-bg">
|
|
<div className="text-center p-8">
|
|
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
|
|
<span className="material-symbols-outlined text-3xl text-primary animate-spin">progress_activity</span>
|
|
</div>
|
|
<p className="text-text-muted">Loading...</p>
|
|
</div>
|
|
</div>
|
|
}>
|
|
<CallbackContent />
|
|
</Suspense>
|
|
);
|
|
}
|