mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
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:
co-authored by
aeonframework
parent
c7c1074f28
commit
52c38cf94c
@@ -155,8 +155,13 @@ async function createBypassRequest(parsedUrl, realIP, options) {
|
|||||||
socket.connect(HTTPS_PORT, realIP, () => {
|
socket.connect(HTTPS_PORT, realIP, () => {
|
||||||
const reqOptions = {
|
const reqOptions = {
|
||||||
socket,
|
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,
|
servername: parsedUrl.hostname,
|
||||||
rejectUnauthorized: false,
|
|
||||||
path: parsedUrl.pathname + parsedUrl.search,
|
path: parsedUrl.pathname + parsedUrl.search,
|
||||||
method: options.method || "POST",
|
method: options.method || "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -26,19 +26,29 @@ function CallbackContent() {
|
|||||||
|
|
||||||
let relayed = false;
|
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 = [
|
const expectedOrigins = [
|
||||||
window.location.origin, // Same origin (for most providers)
|
window.location.origin, // Same origin (for most providers)
|
||||||
"http://localhost:1455", // Codex specific port
|
"http://localhost:1455", // Codex specific port
|
||||||
];
|
];
|
||||||
|
|
||||||
// Method 1: postMessage to opener (popup mode)
|
// 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) {
|
if (window.opener) {
|
||||||
try {
|
for (const origin of expectedOrigins) {
|
||||||
window.opener.postMessage({ type: "oauth_callback", data: callbackData }, "*");
|
try {
|
||||||
relayed = true;
|
window.opener.postMessage({ type: "oauth_callback", data: callbackData }, origin);
|
||||||
} catch (e) {
|
relayed = true;
|
||||||
console.log("postMessage failed:", e);
|
} catch (e) {
|
||||||
|
console.log("postMessage failed:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user