fix(chat): send the CSRF double-submit token from useChat - #3611
Conversation
A production build defaults `security.csrf` to on, so `CsrfHandler` rejects any POST whose `x-csrf-token` header does not match the `__Host-vf_csrf` cookie. `useChat` sent `Content-Type` and the caller's headers and nothing else, so every browser chat turn in a production deployment answered `403 Forbidden – invalid or missing CSRF token` and the UI showed "API error: 403" with a Retry button. `veryfront dev` leaves CSRF off, so the break only appeared after `veryfront build` — the exact step the getting-started walk tells people to take. The `ai-agent` template ships chat as its only feature, so the documented deploy journey produced a dead app. The server half was already correct: HTML responses set a JS-readable `__Host-vf_csrf` cookie, and the same request with the header attached gets past `CsrfHandler`. Only the browser half was missing. `workflowMutationHeaders` already implemented exactly this for workflow mutations. Lift it to `security/csrf/browser-mutation-headers.ts` — a zero-dependency leaf safe for client bundles — and call it from the AG-UI POST. It stays a no-op on the server, when the caller set the header itself, when the endpoint leaves the document origin, and when no cookie exists.
|
Warning Review limit reached
Next review available in: 52 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
Comment |
#3611 fixed the AG-UI chat turn, but `useAttachments` is a separate transport: it issues its own `POST {url}` to upload and `DELETE {url}?id=` to remove, and both went out with no `x-csrf-token`. A deployed chat *with attachments* therefore still answered 403 after #3611. `veryfront dev` does not enable CSRF, so this only ever showed up in production. Both mutations now route their headers through `csrfMutationHeaders` — the helper #3611 already extracted to `security/csrf/browser-mutation-headers.ts`. No new CSRF implementation is added here. The list `GET` is a safe method and is left alone. The tests drive the real hook and pipe whatever it emits through the real `CsrfHandler` with `securityConfig: { csrf: true }`, so they fail on an actual 403 rather than on a header assertion.
#3611 fixed the AG-UI chat turn, but attachments travel over two *other* transports and neither sent the token: - `useUpload` — the one `<Chat uploadApi>` actually wires up (`app-mode-chat.tsx:113`, `controlled-chat.tsx:99`). It uploads over `XMLHttpRequest`, since fetch has no upload-progress event, and applied only caller-supplied headers. - `useAttachments` — the durable uploads registry exported from `veryfront/chat`. `POST {url}` to upload, `DELETE {url}?id=` to remove. A deployed chat *with attachments* therefore still answered 403 after #3611. `veryfront dev` does not enable CSRF, so this only ever showed up in production. All three mutations now route their headers through `csrfMutationHeaders` — the helper #3611 already extracted to `security/csrf/browser-mutation-headers.ts`. No new CSRF implementation is added here. The registry's list `GET` is a safe method and is left alone. The registry `DELETE` passes its real `?id=` target so the helper's same-origin guard evaluates the URL actually being hit. The tests drive the real hooks — including a fake `XMLHttpRequest` that replays whatever `useUpload` sets into a real `Request` — and pipe the result through the real `CsrfHandler` with `securityConfig: { csrf: true }`, so they fail on an actual 403 rather than on a header assertion.
#3611 fixed the AG-UI chat turn, but attachments travel over two *other* transports and neither sent the token: - `useUpload` — the one `<Chat uploadApi>` actually wires up (`app-mode-chat.tsx:113`, `controlled-chat.tsx:99`). It uploads over `XMLHttpRequest`, since fetch has no upload-progress event, and applied only caller-supplied headers. - `useAttachments` — the durable uploads registry exported from `veryfront/chat`. `POST {url}` to upload, `DELETE {url}?id=` to remove. A deployed chat *with attachments* therefore still answered 403 after #3611. `veryfront dev` does not enable CSRF, so this only ever showed up in production. All three mutations now route their headers through `csrfMutationHeaders` — the helper #3611 already extracted to `security/csrf/browser-mutation-headers.ts`. No new CSRF implementation is added here. The registry's list `GET` is a safe method and is left alone. The registry `DELETE` passes its real `?id=` target so the helper's same-origin guard evaluates the URL actually being hit. The tests drive the real hooks — including a fake `XMLHttpRequest` that replays whatever `useUpload` sets into a real `Request` — and pipe the result through the real `CsrfHandler` with `securityConfig: { csrf: true }`, so they fail on an actual 403 rather than on a header assertion.
Symptom
Follow the documented
ai-agentjourney end to end and the app you ship is dead:type a message, get "API error: 403" and a Retry button.
POST /api/ag-uireturns
403 Forbidden – invalid or missing CSRF token.veryfront devworksfine on the identical project, so the break only appears after
veryfront build— the exact step getting-started tells people to take. Chat is the template's
only feature, so the deploy journey produces a broken app.
Reproduced on published 0.1.1229 (and 0.1.1228 — not a release regression).
Root cause
A production build defaults
security.csrfto on(
src/security/http/config.ts:271), soCsrfHandlerrequiresx-csrf-tokento match the
__Host-vf_csrfcookie on every non-GET/HEAD/OPTIONS request.The server half is already correct — an HTML response sets a JS-readable
__Host-vf_csrfcookie (applyCsrfCookie,httpOnly: false), and the same POSTwith the header attached sails through CSRF. The browser half was missing:
useChat's AG-UI fetch sentContent-Typeplus the caller's headers and nothingelse.
Verified against the published artifact — the module 0.1.1229 serves to the browser:
Fix
workflowMutationHeadersalready did exactly this for workflow mutations. Liftedto
src/security/csrf/browser-mutation-headers.ts(a zero-dependency leaf, safefor client bundles;
workflowMutationHeadersnow delegates to it) and called fromthe AG-UI POST. It no-ops on the server, when the caller already set the header,
when the endpoint is cross-origin, and when there is no cookie — the server stays
fail-closed either way.
Proof the original symptom is gone
Same project, same browser flow (agent-browser: open, type "hello", Enter), two
servers side by side:
/api/ag-uiveryfront servex-csrf-token: B4l0eejEXxz…(from HAR)The 503 is the sandbox having no LLM key; the CSRF wall is gone. Server-side
double-check on the published build:
POSTwith cookie only → 403; identicalPOSTwith cookie and header → past CSRF.Tests
src/agent/react/use-chat/use-chat.csrf.test.tsx— new. Fails on the pre-fixcode with
x-csrf-token=null. Covers: token attached from the cookie, nocookie → no header, caller-supplied header wins, cross-origin endpoint gets
nothing.
Not in scope (same trap, separate symptoms, no repro here)
use-uploads-registry.ts(attachment upload POST / remove DELETE) anduse-completion.ts(POST) issue browser mutations without the token too, so theywill 403 the same way in production. Left alone deliberately — untested behaviour changes in a fix PR
are how symptoms survive green CI. Worth a follow-up with its own repro.