feat: CSRF protection on the login and URL-generator forms (closes #93)
check / check (push) Failing after 1s
check / check (push) Failing after 1s
Both cookie-authenticated HTML form posts (POST / and POST /generate) now require a CSRF token via gorilla/csrf, the recorded default in GO_PACKAGE_DEFAULTS.md. The token cookie is independent of the session cookie, so it also covers the login POST, where no session exists yet (login CSRF). The token key is derived from the signing key with its own HKDF salt, so tokens survive restarts and reuse no other key material; gorilla/csrf supplies crypto/rand generation and constant-time compare. The form routes sit in a chi group behind the middleware; the hidden token field is rendered into login.html and generator.html. In local plaintext HTTP mode (debug) requests are marked plaintext so the library does not demand an https Referer or set a Secure cookie the browser would withhold; in production, behind the TLS-terminating proxy, it enforces its https Referer origin check. Model: opus-4-8
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/seal"
|
||||
)
|
||||
|
||||
// csrfKeySalt provides domain separation for the CSRF authentication key,
|
||||
// derived from the signing key so tokens survive restarts without extra
|
||||
// configuration and never reuse the session or encrypted-URL key material.
|
||||
const csrfKeySalt = "pixa-csrf-v1"
|
||||
|
||||
// newCSRFProtect builds the CSRF-protection middleware for the
|
||||
// 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).
|
||||
//
|
||||
// When plaintext is true (local HTTP development), requests are marked
|
||||
// plaintext so the library neither demands an https Referer nor sets a
|
||||
// Secure cookie the browser would withhold over http. In production the
|
||||
// service runs behind a TLS-terminating proxy, so plaintext is false and
|
||||
// the library enforces its https Referer origin check.
|
||||
func newCSRFProtect(
|
||||
signingKey string, plaintext bool,
|
||||
) (func(http.Handler) http.Handler, error) {
|
||||
key, err := seal.DeriveKey([]byte(signingKey), csrfKeySalt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protect := csrf.Protect(
|
||||
key[:],
|
||||
csrf.Path("/"),
|
||||
csrf.Secure(!plaintext),
|
||||
csrf.SameSite(csrf.SameSiteStrictMode),
|
||||
)
|
||||
|
||||
if !plaintext {
|
||||
return protect, nil
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
protected := protect(next)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
protected.ServeHTTP(w, csrf.PlaintextHTTPRequest(r))
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CSRF returns the CSRF-protection middleware for the login and generator
|
||||
// form routes.
|
||||
func (s *Handlers) CSRF() func(http.Handler) http.Handler {
|
||||
return s.csrfProtect
|
||||
}
|
||||
|
||||
// csrfField returns the hidden form input carrying the CSRF token for the
|
||||
// given request, to be embedded verbatim in a rendered form.
|
||||
func csrfField(r *http.Request) template.HTML {
|
||||
return csrf.TemplateField(r)
|
||||
}
|
||||
Reference in New Issue
Block a user