50 lines
2.2 KiB
Go
50 lines
2.2 KiB
Go
package main
|
|
|
|
// 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(`<input type="hidden" name="` + FormTokenField + `" value="` +
|
|
template.HTMLEscapeString(sess.Token) + `">`)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|