munin/internal/auth/csrf.go
2026-10-19 10:13:56 +02:00

66 lines
2.4 KiB
Go

package auth
import (
"crypto/subtle"
"net/http"
)
// CSRFFieldName ist der Name des versteckten Formularfelds in den Templates.
const CSRFFieldName = "csrf_token"
// CSRFHeaderName erlaubt es, das Token bei einem fetch()-Aufruf im Header statt
// im Formular zu schicken.
const CSRFHeaderName = "X-CSRF-Token"
// CSRFToken berechnet das Token zu einer Sitzung.
//
// # Wogegen schützt das?
//
// CSRF ("Cross-Site Request Forgery") nutzt aus, dass der Browser Cookies
// automatisch mitschickt. Eine fremde Seite kann ein Formular auf unseren Server
// abschicken lassen, und für den Server sieht es aus wie eine Aktion des
// angemeldeten Nutzers:
//
// <form method="POST" action="https://munin.intern/projects/x/delete">
// <script>document.forms[0].submit()</script>
//
// Das Token unterbindet das: Die fremde Seite kennt es nicht und kann es wegen
// der Same-Origin-Policy des Browsers auch nicht auslesen.
//
// Das Präfix "csrf:" sorgt für Domänentrennung — dasselbe Geheimnis signiert auch
// Session-Cookies, und eine Signatur aus dem einen Zusammenhang darf im anderen
// nicht gelten.
func (s *Signer) CSRFToken(subject string) string {
return s.sign("csrf:" + subject)
}
// ValidCSRFToken prüft ein übermitteltes Token gegen die Sitzung.
func (s *Signer) ValidCSRFToken(token, subject string) bool {
expected := s.CSRFToken(subject)
return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
}
// CSRFTokenFromRequest holt das Token aus dem Formular oder dem Header.
// SignHint signiert eine Erfolgsmeldung, die über einen Redirect in der
// Adresszeile mitreist (siehe withHint in internal/web). Ohne Signatur ließe
// sich mit einem präparierten Link ein beliebiger Text als Meldung in die
// Oberfläche schmuggeln — etwa „Bitte den Key an … schicken".
//
// Das Präfix "hint:" trennt diese Signaturen von Sitzungen und CSRF-Tokens:
// Eine signierte Meldung taugt nie als gültige Sitzung oder umgekehrt.
func (s *Signer) SignHint(message string) string {
return s.sign("hint:" + message)
}
// ValidHint prüft, ob signature zu message passt — in konstanter Zeit, wie bei
// den anderen Vergleichen mit Geheimnissen.
func (s *Signer) ValidHint(message, signature string) bool {
return subtle.ConstantTimeCompare([]byte(signature), []byte(s.SignHint(message))) == 1
}
func CSRFTokenFromRequest(r *http.Request) string {
if token := r.Header.Get(CSRFHeaderName); token != "" {
return token
}
return r.FormValue(CSRFFieldName)
}