Compare commits

..
1 Commits
Author SHA1 Message Date
sneak b4501fdd89 Name the cause on the page for a refused form (closes #358)
check / check (push) Canceled after 0s
The CSRF middleware answered a refused login or generator form with the
library's bare text, such as "Forbidden - origin invalid". It now answers
403 with a page that names the cause and what to do, and logs a warning
with the request ID and the library's reason. A page opened over plain
HTTP with debug off and a form from another host both fail the origin
check; the page tells them apart by the host in the Origin or Referer
header.

Model: opus-5-5
2026-11-11 20:33:31 +00:00
4 changed files with 11 additions and 14 deletions

No files matched your search

+1
View File
@@ -70,3 +70,4 @@ vendor/
# Local dev configs
config.yml
config.yaml
config.dev.yml
+4 -7
View File
@@ -79,15 +79,12 @@ docker-versioned:
docker-test:
@script/test
# Run local dev server in Docker, with debug on and a new random signing
# key each run. Log in to the page with the key it prints.
# Run local dev server in Docker
devserver: docker-versioned devserver-stop
@key="$$(head -c 32 /dev/urandom | base64)" && \
docker run -d --name pixad-dev -p 8080:8080 \
-e PIXA_SIGNING_KEY="$$key" -e PIXA_DEBUG=true \
pixa:latest >/dev/null && \
echo "pixad running at http://localhost:8080" && \
echo "signing key: $$key"
-v $(CURDIR)/config.dev.yml:/etc/pixa/config.yml:ro \
pixa:latest
@echo "pixad running at http://localhost:8080"
# Stop dev server
devserver-stop:
+1 -2
View File
@@ -377,8 +377,7 @@ session cookie is always marked `Secure`. Over plain HTTP a browser keeps such a
cookie only for its own machine (`localhost`), if at all.
A form refused with 403 gets a page that names the cause and what to do about
it. pixa logs a `form refused` warning with the request ID and the form-token
library's reason, such as `origin invalid`.
it. pixa logs the cause as a warning with the request ID.
The image routes answer the errors listed for them with a JSON body holding
`error`, `status` and `timestamp`.
+5 -5
View File
@@ -36,8 +36,8 @@ const (
// state-mutating HTML form routes. The token lives in its own cookie,
// independent of the session cookie, so it also protects the login POST
// where no session exists yet (login CSRF). key signs that cookie. A
// refused form is answered by the handler that handleFormRefused returns.
// log gets a warning with the library's reason, such as "origin invalid".
// refused form gets the page respondFormRefused renders, and log gets a
// warning naming the cause.
//
// When plaintext is true (local HTTP development), requests are marked
// plaintext so the library neither demands an https Referer nor sets a
@@ -52,7 +52,7 @@ func newCSRFProtect(
csrf.Path("/"),
csrf.Secure(!plaintext),
csrf.SameSite(csrf.SameSiteStrictMode),
csrf.ErrorHandler(handleFormRefused(plaintext, log)),
csrf.ErrorHandler(respondFormRefused(plaintext, log)),
)
if !plaintext {
@@ -68,11 +68,11 @@ func newCSRFProtect(
}
}
// handleFormRefused returns the handler the CSRF middleware calls when it
// respondFormRefused returns the handler the CSRF middleware calls when it
// refuses a request. It logs the library's reason with the request ID and
// answers 403 with a page naming the cause and what to do. A reason that is
// not a refusal, such as a failure to make a token, answers 500.
func handleFormRefused(plaintext bool, log *slog.Logger) http.Handler {
func respondFormRefused(plaintext bool, log *slog.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reason := csrf.FailureReason(r)
requestID := middleware.GetReqID(r.Context())