// 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" }