| 1 |
package app
|
| 2 |
|
| 3 |
import (
|
| 4 |
"context"
|
| 5 |
"crypto/subtle"
|
| 6 |
"net/http"
|
| 7 |
)
|
| 8 |
|
| 9 |
const csrfCookie = "spread_csrf"
|
| 10 |
|
| 11 |
// csrf implements double-submit-cookie CSRF protection: a random token is kept
|
| 12 |
// in an httponly cookie and echoed into every form; unsafe requests must send a
|
| 13 |
// matching csrf_token field. Combined with the SameSite=Lax session/csrf
|
| 14 |
// cookies, this blocks cross-site state changes.
|
| 15 |
func (a *App) csrf(next http.Handler) http.Handler {
|
| 16 |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 17 |
tok := ""
|
| 18 |
if c, err := r.Cookie(csrfCookie); err == nil {
|
| 19 |
tok = c.Value
|
| 20 |
}
|
| 21 |
if tok == "" {
|
| 22 |
tok = randomToken()
|
| 23 |
http.SetCookie(w, &http.Cookie{
|
| 24 |
Name: csrfCookie,
|
| 25 |
Value: tok,
|
| 26 |
Path: "/",
|
| 27 |
HttpOnly: true,
|
| 28 |
Secure: a.secureCookie,
|
| 29 |
SameSite: http.SameSiteLaxMode,
|
| 30 |
})
|
| 31 |
}
|
| 32 |
|
| 33 |
switch r.Method {
|
| 34 |
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
|
| 35 |
if subtle.ConstantTimeCompare([]byte(r.PostFormValue("csrf_token")), []byte(tok)) != 1 {
|
| 36 |
http.Error(w, "invalid or missing CSRF token", http.StatusForbidden)
|
| 37 |
return
|
| 38 |
}
|
| 39 |
}
|
| 40 |
|
| 41 |
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), csrfKey, tok)))
|
| 42 |
})
|
| 43 |
}
|
| 44 |
|
| 45 |
func csrfToken(r *http.Request) string {
|
| 46 |
t, _ := r.Context().Value(csrfKey).(string)
|
| 47 |
return t
|
| 48 |
}
|