{"record":{"id":"62943c0cbc333268","repo":"github/github-mcp-server","slug":"state-mismatch-possible-csrf","errorCode":null,"errorMessage":"state mismatch (possible CSRF)","messagePattern":"state mismatch \\(possible CSRF\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/oauth/callback.go","lineNumber":97,"sourceCode":"// handler renders the callback endpoint. It reports the outcome exactly once and\n// always shows the user a friendly page.\nfunc (cs *callbackServer) handler(expectedState string) http.Handler {\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/callback\", func(w http.ResponseWriter, r *http.Request) {\n\t\tq := r.URL.Query()\n\n\t\tif errCode := q.Get(\"error\"); errCode != \"\" {\n\t\t\tmsg := errCode\n\t\t\tif desc := q.Get(\"error_description\"); desc != \"\" {\n\t\t\t\tmsg = fmt.Sprintf(\"%s: %s\", errCode, desc)\n\t\t\t}\n\t\t\tcs.report(callbackResult{err: fmt.Errorf(\"authorization failed: %s\", msg)})\n\t\t\trenderError(w, msg)\n\t\t\treturn\n\t\t}\n\n\t\tif q.Get(\"state\") != expectedState {\n\t\t\tcs.report(callbackResult{err: fmt.Errorf(\"state mismatch (possible CSRF)\")})\n\t\t\trenderError(w, \"state mismatch\")\n\t\t\treturn\n\t\t}\n\n\t\tcode := q.Get(\"code\")\n\t\tif code == \"\" {\n\t\t\tcs.report(callbackResult{err: fmt.Errorf(\"no authorization code in callback\")})\n\t\t\trenderError(w, \"no authorization code received\")\n\t\t\treturn\n\t\t}\n\n\t\tcs.report(callbackResult{code: code})\n\t\trenderSuccess(w)\n\t})\n\treturn mux\n}\n\n// report delivers the first outcome and drops later ones (the channel is","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/internal/oauth/callback.go#L79-L115","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if strings.Contains(err.Error(), \"state mismatch\") {\n    // discard the attempt; restart the flow in a fresh tab; treat repeated mismatches as hostile\n}","preventionTips":["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"],"tags":["oauth","csrf","security","state","user-action"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}