{"record":{"id":"3310eba97df53f5a","repo":"hashicorp/nomad","slug":"handshake-auth-token-mismatched-auth-header-token","errorCode":null,"errorMessage":"handshake auth token mismatched auth header token","messagePattern":"handshake auth token mismatched auth header token","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"command/agent/websockets.go","lineNumber":162,"sourceCode":"\t} else if h, err := strconv.ParseBool(hv); err != nil {\n\t\treturn \"\", fmt.Errorf(\"ws_handshake value is not a boolean: %v\", err)\n\t} else if !h {\n\t\treturn \"\", nil\n\t}\n\n\t// verify that any header token set by a non-browser client agrees with the\n\t// auth header\n\treqToken := new(string)\n\ts.parseToken(req, reqToken)\n\n\tvar h wsHandshakeMessage\n\terr := readFn(&h)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif reqToken != nil && *reqToken != \"\" && *reqToken != h.AuthToken {\n\t\treturn \"\", fmt.Errorf(\"handshake auth token mismatched auth header token\")\n\t}\n\n\tsupportedWSHandshakeVersion := 1\n\tif h.Version != supportedWSHandshakeVersion {\n\t\treturn \"\", fmt.Errorf(\"unexpected handshake value: %v\", h.Version)\n\t}\n\n\treturn h.AuthToken, nil\n}\n\n// getWebsocketConnection retrieves the websocket connection from context\nfunc (s *HTTPServer) getWebsocketConnection(req *http.Request) (*websocket.Conn, error) {\n\tctx := req.Context()\n\n\t// Get websocket connection from context (set by audit wrapper)\n\tconnRaw := ctx.Value(ctxKeyWebSocketConn)\n\tif connRaw == nil {\n\t\treturn nil, fmt.Errorf(\"websocket connection not found in context\")","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/command/agent/websockets.go#L144-L180","documentation":"When ws_handshake is enabled, the client must send its auth token both in the HTTP header and in the first handshake message. readWsHandshake rejects the connection when the header token and the handshake-message token differ, as a mismatch may indicate CSRF-style header injection from a browser or a compromised/misconfigured client.","triggerScenarios":"readFn decodes a handshake message whose AuthToken differs from the auth header token (reqToken) when reqToken is non-empty — e.g. a browser page that cannot set headers relaying a stale token.","commonSituations":"Browser-based exec where the handshake token was generated from an old ACL token while the page holds a newer header token; proxies that strip or rewrite auth headers; replayed handshake payloads.","solutions":["Send the identical auth token in both the HTTP auth header and the websocket handshake message.","Refresh the client so both tokens derive from the same current ACL token.","Clear cached pages/scripts holding stale tokens and reload the UI.","If a proxy manipulates headers, configure it to pass the auth header through unchanged."],"exampleFix":"// before\nmsg := handshakeMessage{AuthToken: oldToken}\n// after\nmsg := handshakeMessage{AuthToken: currentACLToken} // same value as auth header","handlingStrategy":"validation","validationCode":"if headerToken != \"\" && handshakeToken != headerToken {\n    return fmt.Errorf(\"handshake and header tokens must match\")\n}","typeGuard":null,"tryCatchPattern":"if err := readFn(&h); err != nil {\n    if strings.Contains(err.Error(), \"mismatched auth header token\") {\n        refreshTokenAndRetry()\n    }\n    return err\n}","preventionTips":["Derive handshake token and header token from the same source at call time.","Avoid caching tokens in long-lived browser pages; fetch per connection.","Ensure proxies pass the auth header untouched.","On mismatch, refresh the ACL token and reconnect once, not in a loop."],"tags":["websocket","auth","token","security"],"backgroundTag":"auth-token-mismatch","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}