{"record":{"id":"92a132052302381b","repo":"multica-ai/multica","slug":"csrf-validation-failed","errorCode":null,"errorMessage":"CSRF validation failed","messagePattern":"CSRF validation failed","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"server/internal/middleware/auth.go","lineNumber":59,"sourceCode":"\t\t\t// the client is untrusted and discarded before the auth\n\t\t\t// branches run. Only the mat_ branch below re-sets it. This\n\t\t\t// is what prevents a client from sending a normal mul_ PAT\n\t\t\t// plus a forged `X-Actor-Source: member` (or anything else)\n\t\t\t// to convince a downstream handler that its request came\n\t\t\t// from a non-task-token path.\n\t\t\tr.Header.Del(\"X-Actor-Source\")\n\n\t\t\ttokenString, fromCookie := extractToken(r)\n\t\t\tif tokenString == \"\" {\n\t\t\t\tslog.Debug(\"auth: no token found\", \"path\", r.URL.Path)\n\t\t\t\thttp.Error(w, `{\"error\":\"missing authorization\"}`, http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Cookie-based auth requires CSRF validation for state-changing methods.\n\t\t\tif fromCookie && !auth.ValidateCSRF(r) {\n\t\t\t\tslog.Debug(\"auth: CSRF validation failed\", \"path\", r.URL.Path)\n\t\t\t\thttp.Error(w, `{\"error\":\"CSRF validation failed\"}`, http.StatusForbidden)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Agent task token: \"mat_\" prefix. Minted by the server at\n\t\t\t// task-claim time and injected by the daemon into the agent\n\t\t\t// process. Authoritative for actor identity — the bound\n\t\t\t// (user_id, agent_id, task_id, workspace_id) triple is\n\t\t\t// written into request headers here, OVERRIDING whatever the\n\t\t\t// client sent, so a downstream actor-resolver cannot be\n\t\t\t// tricked by a client that strips or forges X-Agent-ID /\n\t\t\t// X-Task-ID. Human-only endpoints (e.g. agent env\n\t\t\t// management) reject requests authenticated this way; see\n\t\t\t// `actorSourceFromRequest`. MUL-2600.\n\t\t\tif strings.HasPrefix(tokenString, \"mat_\") {\n\t\t\t\tif queries == nil {\n\t\t\t\t\thttp.Error(w, `{\"error\":\"invalid token\"}`, http.StatusUnauthorized)\n\t\t\t\t\treturn\n\t\t\t\t}","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/middleware/auth.go#L41-L77","documentation":"HTTP 403 from the auth middleware when the token came from the auth cookie (stateful browser session) and CSRF validation failed for a state-changing method. Cookie-based auth is vulnerable to cross-site request forgery, so mutating requests authenticated via cookie must present the CSRF token/header pair validated by auth.ValidateCSRF; a missing or mismatched CSRF token is rejected with 403 (not 401, because the session itself is valid).","triggerScenarios":"Browser sends a state-changing request (POST/PUT/PATCH/DELETE) authenticated by the session cookie without the CSRF header, with a stale CSRF token, or with a token bound to a different session; a cross-site form/fetch attempting an authenticated action.","commonSituations":"Frontend fetch omits the CSRF header after a session refresh/rotation; CSRF cookie not read correctly (wrong cookie name or SameSite blocks reading); a third-party page attempting CSRF (the rejection working as designed); API clients mistakenly using cookie auth instead of bearer auth.","solutions":["Have the frontend read the CSRF cookie and send it in the expected header on every mutating request.","If the session was refreshed, re-read the CSRF token — tokens from the old session will not validate.","For non-browser clients, switch to Authorization: Bearer <PAT>, which bypasses CSRF checks entirely.","Confirm the request includes credentials (cookies) so the session cookie accompanies the CSRF token."],"exampleFix":"// before: mutating request with cookie but no CSRF header\nawait fetch('/api/issues', {method:'POST', credentials:'include', body})\n// → 403 CSRF validation failed\n\n// after: attach CSRF header from cookie\nconst csrf = document.cookie.match(/csrf_token=(\\w+)/)?.[1] ?? ''\nawait fetch('/api/issues', {method:'POST', credentials:'include',\n  headers:{'X-CSRF-Token':csrf}, body})","handlingStrategy":"validation","validationCode":"function csrfToken(): string {\n  return document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)?.[1] ?? ''\n}\nif (method !== 'GET' && !csrfToken()) throw new Error('missing CSRF token; refresh session')","typeGuard":"function hasCsrf(): boolean { return csrfToken() !== '' }","tryCatchPattern":"const resp = await fetch(url, init)\nif (resp.status === 403) {\n  const body = await resp.text()\n  if (body.includes('CSRF')) { await refreshSession(); retryOnce() } // stale CSRF, not a permission problem\n}","preventionTips":["Attach the CSRF header automatically in a shared fetch wrapper for mutating methods.","Re-read the CSRF cookie after any session refresh or login.","Prefer bearer-token auth for non-browser clients to bypass CSRF entirely."],"tags":["csrf","authentication","http-403","security","cookies"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}