Umsetzung der Ausbaustufen 2.1, 3.6, 3.8, 3.9, 4.1, 4.2, 4.4, 4.6, 5.1–5.4 (Details in AUSBAUSTUFEN.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
4.1 KiB
Go
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"
|
|
}
|