Description
Submitting the new-project form returns 403 CSRF validation failed. from internal/server/ui_csrf.go:116.
Steps to reproduce
- Sign in.
- Go to New project, fill in key and name.
- Submit.
Actual
403, body CSRF validation failed.
Expected
Project created.
Root cause: CSRF tokens are injected entirely by client-side JS, with no server-rendered fallback
internal/server/templates/new_project_panel.html:11 is a plain form with no hidden csrf_token input:
<form method="post" action="/projects" ...>
The only server-rendered CSRF value in the app shell is the meta tag at templates/shell.html:7 (<meta name="csrf-token" content="{{.CSRFToken}}">). internal/server/static/app.js reads it once at load (app.js:3) and injects the hidden field at submit time via a capture-phase listener registered at app.js:978:
document.body.addEventListener("submit", (event) => ensureCSRFFormToken(event.target), true);
Server-side, uiRequestCSRFToken (ui_csrf.go:134-146) accepts either the X-CSRF-Token header or the csrf_token form field. A plain form POST sends no header, so it depends entirely on that JS injection. If the listener has not run, the request carries no token and uiCSRFMiddleware rejects it.
POST /projects and POST /tokens are in the same route group under uiSessionCSRFMiddleware (ui_routes.go:28-39, :61), so the middleware is not the differentiator.
Leading hypothesis for why this form specifically
new_project_panel.html:18 puts autofocus on the key input, and app.js is loaded at the very end of <body> (shell.html:29) without defer. A user can type into the autofocused field and press Enter to submit before app.js executes, so the submit listener is not yet registered, no csrf_token is injected, and the POST is rejected. The tokens page has no autofocus, which is consistent with it working while this one fails.
Fix
Render the token server-side rather than relying on JS. Add <input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> to the form in new_project_panel.html, and do the same across the other 23 templates that contain a form but no csrf_token (found via grep -rln "<form" minus grep -q "csrf_token" — includes tokens.html, settings.html, new_issue_panel.html, project_member_page.html, and the issue/project panel templates). Keep the JS injection as a belt-and-braces path for dynamically built forms.
Add a test asserting every rendered form with a non-GET method contains a csrf_token input.
Secondary cause worth ruling out on the deployment
uiCSRFSourceAllowed (ui_csrf.go:148-169) falls back to scheme + "://" + r.Host when publicOrigin is unset, and derives the scheme from r.TLS. Behind a TLS-terminating proxy (Cloudflare) r.TLS is nil, so expected becomes http://trackslash.com while the browser sends Origin: https://trackslash.com — a scheme mismatch that rejects every unsafe request. Confirm publicOrigin is configured in the deployed environment. If it is not, that alone reproduces this error.
Sub-issues
0Linked issues
0GitHub
0No branches or pull requests linked.