NousResearch/hermes-agent · error · Error
Loopback callback state mismatch (possible CSRF)
Error message
Loopback callback state mismatch (possible CSRF)
What it means
CSRF defense per RFC 6749 §10.12: the desktop generates 'state' when starting the native OAuth flow and requires the loopback callback to return the identical value. A missing state on either side, or any mismatch, throws rather than redeeming the authorization code — because a mismatched callback may be a forged redirect injecting an attacker's code (login CSRF / code injection). This error means the defense worked; the callback should not be trusted.
Source
Thrown at apps/desktop/electron/native-oauth.ts:167
const parsed = new URL(requestUrl, 'http://127.0.0.1')
const error = parsed.searchParams.get('error')
if (error) {
const desc = parsed.searchParams.get('error_description') || ''
throw new Error(`Gateway rejected native login: ${error}${desc ? ` (${desc})` : ''}`)
}
const code = parsed.searchParams.get('code') || ''
const state = parsed.searchParams.get('state') || ''
if (!code) {
throw new Error('Loopback callback missing authorization code')
}
if (!expectedState || state !== expectedState) {
// Never redeem a code that arrived with a mismatched state — it may be a
// forged callback trying to inject an attacker's code.
throw new Error('Loopback callback state mismatch (possible CSRF)')
}
return { code }
}
/**
* Normalize a `/auth/native/token` (or refresh) JSON response into a
* NativeTokenSet, validating the shape. Throws on a missing/short access
* token so a malformed response fails loudly rather than storing junk.
*/
export function parseTokenResponse(body: any): NativeTokenSet {
const accessToken = String(body?.access_token || '')
if (!accessToken) {
throw new Error('Gateway token response missing access_token')
}
const expiresAt = Number(body?.expires_at)View on GitHub (pinned to c896c09c42)
Solutions
- Cancel and restart the login flow from scratch — a mismatched state invalidates that callback by design
- Ensure only one native OAuth flow runs at a time (disable the sign-in button while a flow is pending)
- Avoid reloading the app/window mid-login, which discards expectedState
- If it persists, check that the gateway echoes the state parameter verbatim in its redirect
Example fix
// before
const startBtn.onclick = () => void runNativeOAuth() // double-click spawns two flows
// after
let flowRunning = false
const startBtn.onclick = () => { if (flowRunning) return; flowRunning = true; void runNativeOAuth().finally(() => { flowRunning = false }) } Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure exactly one flow is live before comparing state
if (activeFlow) throw new Error('A login flow is already in progress')
const expectedState = crypto.randomUUID()
activeFlow = { expectedState }
const cb = parseLoopbackCallback(requestUrl, expectedState) Type guard
function isStateMismatch(e: unknown): boolean { return e instanceof Error && e.message.includes('state mismatch') } Try / catch
try { const { code } = parseLoopbackCallback(requestUrl, expectedState) } catch (e) { if (isStateMismatch(e)) { cancelFlow(); promptRelogin('Login session expired or forged — restarting sign-in') } else throw e } Prevention
- Serialize login flows — never run two native OAuth flows concurrently
- Never redeem a code from a callback whose state doesn't match; always restart the flow
- Keep expectedState in durable memory for the flow's lifetime (no reloads mid-login)
When it happens
Trigger: Callback's state param differs from the one generated at flow start: two concurrent login attempts where attempt B's callback is checked against attempt A's expectedState; a manually crafted/bookmarked callback URL; expectedState lost because the flow object was recreated (window/app reload mid-flow); a forged redirect from a hostile page.
Common situations: Double-clicking 'sign in' spawning two loopback listeners; renderer reload during login; an attacker or buggy integration redirecting to 127.0.0.1 with their own code+state.
Related errors
- Gateway rejected native login: ${error}${desc ? ` (${desc})`
- Loopback callback missing authorization code
- Gateway token response missing access_token
- File uploads are not supported against OAuth-gated remote ba
- Invalid external URL
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/506ebf0e492aea1e.
Report an issue: GitHub.