{"record":{"id":"6642703b4c950a1d","repo":"decolua/9router","slug":"cross-origin-callback-rejected","errorCode":null,"errorMessage":"Cross-origin callback rejected","messagePattern":"Cross-origin callback rejected","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"src/lib/oauth/utils/server.js","lineNumber":483,"sourceCode":"      return;\n    }\n    const server = http.createServer(async (req, res) => {\n      const url = new URL(req.url, \"http://localhost\");\n      if (url.pathname !== TRAE_CONFIG.callbackPath && url.pathname !== \"/auth/callback\") {\n        res.writeHead(404);\n        res.end(\"Not found\");\n        return;\n      }\n      const session = traeSession;\n      if (!session) {\n        res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n        res.end(renderCodexResultPage(false, \"No active Trae login session\"));\n        return;\n      }\n      // Anti-CSRF: reject cross-origin fetches (legit redirects send no Origin),\n      // and reject state mismatch when state is present.\n      if (!isLoopbackOrigin(req.headers.origin)) {\n        res.writeHead(403, { \"Content-Type\": \"text/html; charset=utf-8\" });\n        res.end(renderCodexResultPage(false, \"Cross-origin callback rejected\"));\n        return;\n      }\n      const cbState = url.searchParams.get(\"state\");\n      if (cbState && session.state && cbState !== session.state) {\n        session.status = \"error\";\n        session.error = \"Trae callback state mismatch\";\n        res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n        res.end(renderCodexResultPage(false, session.error));\n        stopTraeProxy();\n        return;\n      }\n      // Pass the raw callback query to exchangeTokens → parseTraeCallback\n      const rawCallback = `${url.pathname}?${url.searchParams.toString()}`;\n      try {\n        const { exchangeTokens } = await import(\"../providers.js\");\n        const { createProviderConnection } = await import(\"@/models\");\n        const tokenData = await exchangeTokens(\"trae\", rawCallback);","sourceCodeStart":465,"sourceCodeEnd":501,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/utils/server.js#L465-L501","documentation":"startTraeProxy enforces an anti-CSRF check on the OAuth callback: requests must either carry no Origin header (like a genuine browser redirect from a top-level navigation) or have an Origin that resolves to a loopback host. A request with a cross-origin Origin header is answered 403 with 'Cross-origin callback rejected'. This prevents a malicious web page from scripting fetches against the loopback callback endpoint to inject forged authorization codes.","triggerScenarios":"A browser page on a non-loopback origin (e.g. http://evil.example) issues a fetch/XHR to http://127.0.0.1:<traePort>/...callback during an active Trae session; CORS preflight or credentialed cross-site calls; misconfigured redirect landing from a non-loopback host header context that sets Origin.","commonSituations":"Clickjacking/phishing page attempting a CSRF login attack; browser extensions proxying requests with an Origin header; corporate proxies that rewrite requests and attach an external Origin; developer testing the callback via a forwarded public tunnel (ngrok) whose Origin is not loopback.","solutions":["Complete the login through the normal redirect flow — top-level redirects send no Origin header and pass the check.","If testing through a tunnel, test against http://127.0.0.1 directly instead of the public tunnel URL.","If an extension/proxy injects an Origin header, disable it or exclude localhost from its rewriting rules."],"exampleFix":"// before (test via curl with a foreign Origin)\ncurl -H 'Origin: https://evil.example' 'http://127.0.0.1:3456/callback?code=...&state=...'\n// after\ncurl 'http://127.0.0.1:3456/callback?code=...&state=...'  // no Origin header → accepted","handlingStrategy":"try-catch","validationCode":"const origin = req.headers.origin;\nif (origin && !['http://127.0.0.1','http://localhost'].some(h => origin.startsWith(h))) {\n  // cross-origin scripted request — will be rejected; use a top-level redirect instead\n}","typeGuard":"function isLoopbackOrigin(origin) {\n  if (!origin) return true; // redirects send no Origin\n  try { const u = new URL(origin); return ['127.0.0.1','localhost','[::1]'].includes(u.hostname); }\n  catch { return false; }\n}","tryCatchPattern":"const res = await fetch(callbackUrl, { redirect: 'manual' });\nif (res.status === 403 && (await res.text()).includes('Cross-origin callback rejected')) {\n  console.warn('CSRF guard fired — complete login via normal redirect, not scripted fetch');\n}","preventionTips":["Never fetch/post to the loopback callback URL from browser scripts or other tabs.","Disable extensions/proxies that add Origin headers to localhost traffic.","Test callbacks with plain curl (no Origin header) or via the app's real redirect.","If using tunnels for testing, drive the browser through http://127.0.0.1, not the tunnel URL."],"tags":["csrf","oauth","http-403"],"backgroundTag":"csrf-origin-rejected","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}