multica-ai/multica · error

CSRF validation failed

Error message

CSRF validation failed

What it means

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).

Source

Thrown at server/internal/middleware/auth.go:59

			// the client is untrusted and discarded before the auth
			// branches run. Only the mat_ branch below re-sets it. This
			// is what prevents a client from sending a normal mul_ PAT
			// plus a forged `X-Actor-Source: member` (or anything else)
			// to convince a downstream handler that its request came
			// from a non-task-token path.
			r.Header.Del("X-Actor-Source")

			tokenString, fromCookie := extractToken(r)
			if tokenString == "" {
				slog.Debug("auth: no token found", "path", r.URL.Path)
				http.Error(w, `{"error":"missing authorization"}`, http.StatusUnauthorized)
				return
			}

			// Cookie-based auth requires CSRF validation for state-changing methods.
			if fromCookie && !auth.ValidateCSRF(r) {
				slog.Debug("auth: CSRF validation failed", "path", r.URL.Path)
				http.Error(w, `{"error":"CSRF validation failed"}`, http.StatusForbidden)
				return
			}

			// Agent task token: "mat_" prefix. Minted by the server at
			// task-claim time and injected by the daemon into the agent
			// process. Authoritative for actor identity — the bound
			// (user_id, agent_id, task_id, workspace_id) triple is
			// written into request headers here, OVERRIDING whatever the
			// client sent, so a downstream actor-resolver cannot be
			// tricked by a client that strips or forges X-Agent-ID /
			// X-Task-ID. Human-only endpoints (e.g. agent env
			// management) reject requests authenticated this way; see
			// `actorSourceFromRequest`. MUL-2600.
			if strings.HasPrefix(tokenString, "mat_") {
				if queries == nil {
					http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
					return
				}

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Have the frontend read the CSRF cookie and send it in the expected header on every mutating request.
  2. If the session was refreshed, re-read the CSRF token — tokens from the old session will not validate.
  3. For non-browser clients, switch to Authorization: Bearer <PAT>, which bypasses CSRF checks entirely.
  4. Confirm the request includes credentials (cookies) so the session cookie accompanies the CSRF token.

Example fix

// before: mutating request with cookie but no CSRF header
await fetch('/api/issues', {method:'POST', credentials:'include', body})
// → 403 CSRF validation failed

// after: attach CSRF header from cookie
const csrf = document.cookie.match(/csrf_token=(\w+)/)?.[1] ?? ''
await fetch('/api/issues', {method:'POST', credentials:'include',
  headers:{'X-CSRF-Token':csrf}, body})
Defensive patterns

Strategy: validation

Validate before calling

function csrfToken(): string {
  return document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)?.[1] ?? ''
}
if (method !== 'GET' && !csrfToken()) throw new Error('missing CSRF token; refresh session')

Type guard

function hasCsrf(): boolean { return csrfToken() !== '' }

Try / catch

const resp = await fetch(url, init)
if (resp.status === 403) {
  const body = await resp.text()
  if (body.includes('CSRF')) { await refreshSession(); retryOnce() } // stale CSRF, not a permission problem
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/92a132052302381b. Report an issue: GitHub.