package web import ( "net" "net/http" "strings" ) // contentSecurityPolicy ist so restriktiv möglich, weil sämtliche Assets // eingebettet und gleich-origin ausgeliefert werden. Es gibt keine externen // Ressourcen und kein Inline-Script. const contentSecurityPolicy = "default-src 'self'; " + "script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; " + "connect-src 'none'; object-src 'none'; base-uri 'none'; " + "form-action 'self'; frame-ancestors 'none'" // SecurityHeaders setzt die Sicherheits-Header auf jede Antwort. func SecurityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := w.Header() h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") h.Set("X-Content-Type-Options", "nosniff") h.Set("Referrer-Policy", "no-referrer") h.Set("X-Frame-Options", "DENY") h.Set("Content-Security-Policy", contentSecurityPolicy) next.ServeHTTP(w, r) }) } // NoStore verhindert jegliches Zwischenspeichern. func NoStore(w http.ResponseWriter) { w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, private") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") } // ClientIP liefert die Quell-IP der Verbindung. X-Forwarded-For wird bewusst // ignoriert: das Portal terminiert TLS selbst, ein gesetzter Header wäre // fälschbar und würde das Audit-Log entwerten. func ClientIP(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return strings.TrimSpace(r.RemoteAddr) } return host }