github/github-mcp-server · error
state mismatch (possible CSRF)
Error message
state mismatch (possible CSRF)
What it means
The state query parameter on the callback did not match the random state generated in beginPKCE (randomState + constant-time-ish comparison via != at internal/oauth/callback.go:94). State mismatch classically indicates CSRF on the authorization-code redirect, but also fires on benign races: a stale browser tab replaying an old redirect, or a double callback from browser prefetch.
Source
Thrown at internal/oauth/callback.go:97
// handler renders the callback endpoint. It reports the outcome exactly once and
// always shows the user a friendly page.
func (cs *callbackServer) handler(expectedState string) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if errCode := q.Get("error"); errCode != "" {
msg := errCode
if desc := q.Get("error_description"); desc != "" {
msg = fmt.Sprintf("%s: %s", errCode, desc)
}
cs.report(callbackResult{err: fmt.Errorf("authorization failed: %s", msg)})
renderError(w, msg)
return
}
if q.Get("state") != expectedState {
cs.report(callbackResult{err: fmt.Errorf("state mismatch (possible CSRF)")})
renderError(w, "state mismatch")
return
}
code := q.Get("code")
if code == "" {
cs.report(callbackResult{err: fmt.Errorf("no authorization code in callback")})
renderError(w, "no authorization code received")
return
}
cs.report(callbackResult{code: code})
renderSuccess(w)
})
return mux
}
// report delivers the first outcome and drops later ones (the channel isView on GitHub (pinned to 0ea1f775a7)
Solutions
- Start the login flow again and complete it in a single fresh browser tab — most mismatches are stale redirects
- If it recurs, confirm only one server instance is running on the callback port (lsof -tiTCP:PORT)
- Keep the window between starting login and clicking Authorize short; do not restart the server mid-flow
- If unexpected callbacks persist, treat them as hostile: verify the state value in the URL matches the one the current flow logged
Defensive patterns
Strategy: try-catch
Try / catch
if strings.Contains(err.Error(), "state mismatch") {
// discard the attempt; restart the flow in a fresh tab; treat repeated mismatches as hostile
} Prevention
- Complete each login in a single tab; close stale authorization tabs before retrying
- Never run two instances sharing one callback port
- Do not bookmark or replay callback URLs
When it happens
Trigger: The /callback request carries a state that differs from the one bound to this listener: an old authorization redirect hitting a server that has since restarted with fresh state; a second server instance on the same registered port receiving the redirect; an attacker-crafted callback link; browser extensions/prefetch re-firing the redirect URL after the first (successful) one already consumed the single-slot channel.
Common situations: User has the auth URL open in two tabs and authorizes in the second; server restarted while the browser sat on GitHub's consent page; port re-use by another instance (ties into error 109); security software replaying URLs.
Related errors
- authorization failed: %s
- no authorization code in callback
- OAuth callback port %d is not available; another process may
- exchanging authorization code: %w
- awaiting device authorization: %w
AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15).
Data as JSON: /api/errors/62943c0cbc333268.
Report an issue: GitHub.