package web // The form token bound to the session (novox/hq ADR 0440 §3's last clause, issue 1284): every form that // changes something — today mail_send, once issue 1267 builds the write page — carries a hidden field with // the session's own token (a random 32 bytes, made once per session, Sessions.Create); a POST missing it, or // carrying another session's, is refused 403 before the change it would have made ever runs, compared in // constant time so the check is never a timing side-channel. import ( "crypto/subtle" "html/template" "net/http" ) // FormTokenField is the hidden field's own name every changing form carries. const FormTokenField = "csrf_token" // FormTokenInput is the hidden field a changing form's own template includes (issue 1284/AC1): bound to the // session handed to the page that renders it (RequireSession, SessionFrom). func FormTokenInput(sess Session) template.HTML { return template.HTML(``) } // RequireFormToken wraps a POST handler that changes something (novox/hq issue 1284): it calls next only once // the session RequireSession handed on (SessionFrom) and the form's own FormTokenField compare equal, in // constant time; a POST with no token, or another session's, is answered 403 and next is never called, so the // change it would have made — today mail_send — never runs (AC2, AC3). RequireFormToken is placed inside // RequireSession (RequireSession(RequireFormToken(next))): a POST with no session at all is the sign-in // redirect RequireSession itself gives, never reaching here. func RequireFormToken(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sess, ok := SessionFrom(r) if !ok { http.Error(w, "forbidden", http.StatusForbidden) return } if err := r.ParseForm(); err != nil { http.Error(w, "forbidden", http.StatusForbidden) return } given := r.PostFormValue(FormTokenField) if given == "" || subtle.ConstantTimeCompare([]byte(given), []byte(sess.Token)) != 1 { http.Error(w, "forbidden", http.StatusForbidden) return } next(w, r) } }