{"record":{"id":"a5ff210eb53dbccf","repo":"Kuberwastaken/claurst","slug":"oauth-state-mismatch-possible-csrf-attack-a5ff21","errorCode":null,"errorMessage":"OAuth state mismatch — possible CSRF attack","messagePattern":"OAuth state mismatch — possible CSRF attack","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-rust/crates/cli/src/oauth_flow.rs","lineNumber":350,"sourceCode":"    let received_state = parsed_url\r\n        .query_pairs()\r\n        .find(|(k, _)| k == \"state\")\r\n        .map(|(_, v)| v.to_string());\r\n\r\n    // Send success redirect to the browser before validating. The same success\r\n    // page is shown on both the valid and error paths (browser UX); request\r\n    // validation happens after this redirect is written.\r\n    let location = oauth::CLAUDEAI_SUCCESS_URL;\r\n\r\n    let response = format!(\r\n        \"HTTP/1.1 302 Found\\r\\nLocation: {}\\r\\nContent-Length: 0\\r\\nConnection: close\\r\\n\\r\\n\",\r\n        location\r\n    );\r\n    writer.write_all(response.as_bytes()).await?;\r\n\r\n    // Validate\r\n    if received_state.as_deref() != Some(expected_state) {\r\n        bail!(\"OAuth state mismatch — possible CSRF attack\");\r\n    }\r\n    let code = code.context(\"No authorization code in callback\")?;\r\n\r\n    Ok(code)\r\n}\r\n\r\n/// Read a single line from stdin (for manual code paste).\r\nasync fn read_line_from_stdin() -> anyhow::Result<String> {\r\n    print!(\"  Or paste authorization code here: \");\r\n    use std::io::Write;\r\n    std::io::stdout().flush().ok();\r\n\r\n    let mut line = String::new();\r\n    let stdin = tokio::io::stdin();\r\n    let mut reader = BufReader::new(stdin);\r\n    reader.read_line(&mut line).await?;\r\n    Ok(line)\r\n}\r","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/Kuberwastaken/claurst/blob/b0637c97ec34144387cbf2f74f65df6d16a6cef1/src-rust/crates/cli/src/oauth_flow.rs#L332-L368","documentation":"run_callback_server validates the state query parameter on the OAuth browser redirect against the state generated when the authorization URL was built. A mismatch means the redirect did not originate from the authorization request this client started — the classic CSRF defense mandated by the OAuth 2.0 spec — so the authorization code is discarded and login aborts.","triggerScenarios":"The browser hits the local /callback endpoint with a state parameter that is missing or differs from expected_state: the user completed an older login tab whose state differs, a different app instance is listening on the same port, the URL was truncated or edited, the state cookie/query was mangled by a proxy or browser extension, or an actual cross-site request forges the callback.","commonSituations":"Developers hit this when running two login flows in parallel (both bind adjacent or reused ports), when a stale browser tab from a previous attempt fires its redirect into the current server, when copying the auth URL partially, or behind corporate proxies that rewrite query strings.","solutions":["Close all stale login tabs and previous CLI sessions, then retry the login flow from scratch.","Ensure no other instance of the tool is running and holding/reusing the same callback port.","Retry the login; each attempt generates a fresh state, so a one-off stale-redirect collision is resolved by a new attempt.","If a proxy or browser extension rewrites query parameters, whitelist the localhost callback URL or bypass the proxy for localhost.","Do not bypass this check — it protects against CSRF; only investigate the environment (proxies, extensions) if it reproduces consistently."],"exampleFix":"// before: treating the mismatch as retryable inside the server loop can accept a forged code\nif received_state.as_deref() != Some(expected_state) {\n    bail!(\"OAuth state mismatch — possible CSRF attack\");\n}\n// after: keep the bail, but include both states to make diagnosis possible\nif received_state.as_deref() != Some(expected_state) {\n    bail!(\n        \"OAuth state mismatch — possible CSRF attack (expected {}..., got {}...)\",\n        &expected_state[..8.min(expected_state.len())],\n        received_state.as_deref().map(|s| &s[..8.min(s.len())]).unwrap_or(\"<none>\")\n    );\n}","handlingStrategy":"validation","validationCode":"// Before starting the flow, ensure a single clean attempt\nassert!(expected_state.len() >= 32, \"state must be cryptographically random\");\n// Ensure no other login session is active on the callback port\nif std::net::TcpListener::bind((\"127.0.0.1\", callback_port)).is_err() {\n    eprintln!(\"callback port in use — close other login sessions first\");\n}","typeGuard":"fn state_matches(received: Option<&str>, expected: &str) -> bool {\n    // constant-time-ish exact compare; both must be present and equal\n    match received {\n        Some(s) => s.len() == expected.len()\n            && s.bytes().zip(expected.bytes()).fold(0u8, |acc, (a, b)| acc | (a ^ b)) == 0,\n        None => false,\n    }\n}","tryCatchPattern":"match wait_for_auth_code_impl(&listener, &state).await {\n    Ok(code) => /* exchange */,\n    Err(e) if e.to_string().contains(\"state mismatch\") => {\n        eprintln!(\"Callback state mismatch: close stale login tabs and retry the login from scratch.\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Close previous login tabs before starting a new OAuth flow","Never run two login flows concurrently on the same machine/port","Do not edit or truncate the authorization URL copied into a browser","Never disable or weaken the state check — it is the CSRF defense","Bypass proxies/extensions for localhost callback URLs if they rewrite queries"],"tags":["oauth","csrf","security","callback"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"b0637c97ec34144387cbf2f74f65df6d16a6cef1","analyzedAt":"2026-09-10T00:24:58.650Z","contentChangedAt":"2026-09-10T00:24:58.650Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}