Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
46 lines
1.6 KiB
Go
46 lines
1.6 KiB
Go
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
|
|
}
|