Skip to content

fix(hub): allow supervisor owner to invoke /:id/scan (CSRF bypass for legacy-JWT)#66

Closed
finedesignz wants to merge 1 commit into
mainfrom
fix/supervisor-scan-403
Closed

fix(hub): allow supervisor owner to invoke /:id/scan (CSRF bypass for legacy-JWT)#66
finedesignz wants to merge 1 commit into
mainfrom
fix/supervisor-scan-403

Conversation

@finedesignz
Copy link
Copy Markdown
Owner

Summary

  • POST /api/supervisors/:id/scan (and every other mutating REST call) returned 403 csrf_failed for users on the legacy-JWT auth path. Hub logs confirmed the affected user (233c6d63-…) was authenticating via method=legacy_jwt.
  • Root cause: hub/src/csrf.ts csrfGuard() enforces double-submit csrf_token cookie + X-CSRF-Token header on every mutating /api/* request. Legacy-JWT sessions never get the csrf_token cookie issued, so the web client (web/src/lib/api.ts) cannot echo it back, and every POST/PUT/PATCH/DELETE fails.
  • Fix: bypass CSRF when the request authenticates via Authorization: Bearer … and carries no session cookie. Bearer-only requests are not CSRF-vulnerable — browsers never auto-attach Authorization headers cross-origin (unlike cookies), so a malicious page cannot forge them.
  • Security boundary preserved: when BOTH a session cookie AND a bearer token are present, CSRF is still enforced (mirrors authMiddleware cookie-wins precedence).

Why this is the right fix (not a security loosening)

CSRF only matters when the browser auto-attaches credentials. The threat model is: attacker.com loads <form action="hub/api/supervisors/.../scan" method=POST>, browser auto-attaches the session cookie, request succeeds. With Bearer tokens this is impossible — JS must explicitly read the token and attach the header, and same-origin-policy blocks attacker.com from reading the legitimate site's storage. The standard treatment of Authorization: Bearer is exactly this bypass.

Test plan

  • bun test hub/test/csrf.test.ts — 20/20 pass (added 2 new cases: bearer-only bypass, bearer+cookie still enforces)
  • After merge + Coolify redeploy: POST /api/supervisors/9a6a325e-.../scan from the web UI returns 200, hub logs show no csrf_failed

🤖 Generated with Claude Code

Root cause: csrfGuard rejected every mutating /api/* request that didn't
carry a csrf_token cookie + matching X-CSRF-Token header. Users on the
legacy-JWT soak path (authMethod=legacy_jwt) never get the csrf_token
cookie issued, so POST /api/supervisors/:id/scan (and every other
mutating REST call) returned 403 csrf_failed.

Fix: skip CSRF enforcement when the request authenticates via
Authorization: Bearer and has no session cookie. Bearer-only requests
are not CSRF-vulnerable because browsers never auto-attach Authorization
headers cross-origin (unlike cookies) — JS must opt-in, which a
malicious cross-origin page cannot do without the token.

When BOTH a session cookie AND a bearer token are present, CSRF is
still enforced (cookie wins — matches authMiddleware precedence).

Added 2 csrf.test.ts cases covering both branches; full suite 20/20.
@finedesignz
Copy link
Copy Markdown
Owner Author

Duplicate of #65 (already merged). Same root cause and same fix shape — main's PR #65 shipped a strictly better implementation (regex-based Bearer detection, richer test coverage, no cookie-presence coupling). Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant