munin/internal/auth/cookies.go
2026-09-15 20:36:26 +02:00

104 lines
4.1 KiB
Go

// Dieses File bündelt die Sicherheits-Einstellungen aller Cookies an einer Stelle.
//
// Vorher setzte jeder Handler seine Cookies selbst und jeder setzte etwas andere
// Flags — der Session-Cookie hatte HttpOnly, aber weder Secure noch SameSite.
// Solche Kleinigkeiten übersieht man leicht, deshalb gibt es jetzt genau eine
// Funktion die ein Cookie "scharf schaltet".
//
// # Die drei Flags und was sie bewirken
//
// - HttpOnly: JavaScript im Browser kann das Cookie nicht auslesen. Schützt
// davor, dass eine XSS-Lücke direkt zur Übernahme der Session führt.
//
// - Secure: Der Browser schickt das Cookie nur über HTTPS. Ohne dieses Flag
// würde ein einziger versehentlicher http://-Aufruf die Session im Klartext
// durchs Netz schicken.
//
// - SameSite: Der Browser schickt das Cookie nicht mit, wenn die Anfrage von
// einer fremden Seite ausgelöst wurde. Das ist die erste Verteidigungslinie
// gegen CSRF (siehe csrf.go für die zweite).
package auth
import (
"net"
"net/http"
"strings"
)
// CookiePolicy entscheidet, ob Cookies mit dem Secure-Flag gesetzt werden.
type CookiePolicy struct {
// explicit ist gesetzt, wenn die Umgebungsvariable COOKIE_SECURE einen
// eindeutigen Wert hat. Ein Zeiger, weil wir drei Zustände brauchen:
// "erzwungen an", "erzwungen aus" und "nicht gesetzt, bitte selbst entscheiden".
explicit *bool
}
// NewCookiePolicy baut die Policy aus dem Wert der Umgebungsvariable COOKIE_SECURE.
//
// Leer (der Normalfall) bedeutet: automatisch entscheiden — siehe Secure.
func NewCookiePolicy(envValue string) CookiePolicy {
switch strings.ToLower(strings.TrimSpace(envValue)) {
case "true", "1", "yes", "on":
yes := true
return CookiePolicy{explicit: &yes}
case "false", "0", "no", "off":
no := false
return CookiePolicy{explicit: &no}
default:
return CookiePolicy{}
}
}
// Secure entscheidet für einen konkreten Request, ob das Secure-Flag gesetzt wird.
//
// Ohne explizite Konfiguration gilt: Secure ist an — außer der Aufruf kommt von
// localhost. Das ist bewusst so herum gewählt:
//
// - In Produktion (eigene Domain, HTTPS) ist das sichere Verhalten der Standard,
// ohne dass man daran denken muss eine Variable zu setzen.
// - Bei der lokalen Entwicklung über http://localhost:8080 würde ein gesetztes
// Secure-Flag den Login kaputt machen (der Browser würde das Cookie verwerfen),
// deshalb dort automatisch aus. Localhost gilt im Browser ohnehin als
// "secure context", es geht also keine Sicherheit verloren.
//
// Wer hinter einem Reverse-Proxy mit TLS-Terminierung fährt, kann mit
// COOKIE_SECURE=true nachhelfen; wer bewusst ohne TLS testet, mit COOKIE_SECURE=false.
func (p CookiePolicy) Secure(r *http.Request) bool {
if p.explicit != nil {
return *p.explicit
}
return !isLocalhost(r)
}
// Apply setzt Pfad und alle Sicherheits-Flags eines Cookies.
// Name, Wert und Ablaufzeit bleiben Sache des Aufrufers.
func (p CookiePolicy) Apply(c *http.Cookie, r *http.Request) {
c.Path = "/"
c.HttpOnly = true
c.Secure = p.Secure(r)
// Lax statt Strict: bei Strict würde ein Link von außen ins Wiki (z.B. aus
// einer Chat-Nachricht) den Nutzer trotz gültiger Session auf der Login-Seite
// landen lassen, weil der Browser das Cookie beim ersten Aufruf zurückhält.
// Lax schickt das Cookie bei normaler Navigation mit, aber nicht bei einem
// von außen ausgelösten POST — genau die Fälle die wir schützen wollen.
c.SameSite = http.SameSiteLaxMode
}
// isLocalhost prüft, ob der Request an localhost gerichtet war.
//
// Ausgewertet wird r.Host (die vom Client angefragte Adresse), nicht r.RemoteAddr:
// Wer über http://localhost:8080 entwickelt, soll das lockere Verhalten bekommen;
// wer über eine echte Domain zugreift, das strenge — auch wenn der Request
// zufällig von derselben Maschine kommt.
func isLocalhost(r *http.Request) bool {
host := r.Host
// Den Port abtrennen, falls einer dranhängt ("localhost:8080" → "localhost").
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
host = strings.ToLower(strings.Trim(host, "[]"))
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}