{"record":{"id":"5f854bb49cfc2c36","repo":"juanfont/headscale","slug":"oidc-state-parameter-is-too-short","errorCode":null,"errorMessage":"oidc state parameter is too short","messagePattern":"oidc state parameter is too short","errorType":"http","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"hscontrol/oidc.go","lineNumber":44,"sourceCode":"const (\n\trandomByteSize           = 16\n\tdefaultOAuthOptionsCount = 3\n\tauthCacheExpiration      = time.Minute * 15\n\n\t// authCacheMaxEntries bounds the OIDC state→[AuthInfo] cache to prevent\n\t// unauthenticated cache-fill DoS via repeated /register/{auth_id} or\n\t// /auth/{auth_id} GETs that mint OIDC state cookies.\n\tauthCacheMaxEntries = 1024\n\n\t// cookieNamePrefixLen is the number of leading characters from a\n\t// state/nonce value that [getCookieName] splices into the cookie name.\n\t// State and nonce values that are shorter than this are rejected at\n\t// the callback boundary so [getCookieName] cannot panic on a slice\n\t// out-of-range.\n\tcookieNamePrefixLen = 6\n)\n\nvar errOIDCStateTooShort = errors.New(\"oidc state parameter is too short\")\n\nvar (\n\terrEmptyOIDCCallbackParams = errors.New(\"empty OIDC callback params\")\n\terrNoOIDCIDToken           = errors.New(\"extracting ID token\")\n\terrNoOIDCRegistrationInfo  = errors.New(\"registration info not in cache\")\n\terrOIDCAllowedDomains      = errors.New(\n\t\t\"authenticated principal does not match any allowed domain\",\n\t)\n\terrOIDCAllowedGroups = errors.New(\"authenticated principal is not in any allowed group\")\n\terrOIDCAllowedUsers  = errors.New(\n\t\t\"authenticated principal does not match any allowed user\",\n\t)\n\terrOIDCUnverifiedEmail = errors.New(\"authenticated principal has an unverified email\")\n\terrInvalidPKCEMethod   = errors.New(\"invalid pkce.method\")\n)\n\n// AuthInfo contains both auth ID and verifier information for OIDC validation.\ntype AuthInfo struct {","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/oidc.go#L26-L62","documentation":"Returned by extractCodeAndStateParamFromRequest (hscontrol/oidc.go:457-459) when the OIDC callback 'state' query parameter is shorter than cookieNamePrefixLen (6 characters). The guard exists because getCookieName splices the first 6 characters of the state into a cookie name; a shorter value would panic with a slice out-of-range. HTTP 400 'invalid state parameter'.","triggerScenarios":"A GET to /oidc/callback (or /register OIDC flow callback) with ?state=abc or any state under 6 chars, e.g. a hand-edited, truncated, or manually constructed callback URL.","commonSituations":"Manual testing of the callback endpoint; misconfigured IdP that echoes a custom/truncated state; a proxy or load balancer truncating query parameters; copy-paste of a callback URL cut off mid-state.","solutions":["Do not construct callback URLs by hand; always start login via the /register endpoint so the IdP echoes back the full server-generated state","Check for URL-truncating proxies/load balancers between the browser and headscale","Verify the IdP's redirect/configuration does not rewrite or shorten the state parameter"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"func validStateLen(state string) bool { return len(state) >= 6 }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never hand-craft OIDC callback URLs","Test proxies do not truncate query strings","Unit-test the callback handler with short states (see oidc_test.go:209)"],"tags":["oidc","auth","http","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}