Files
Aether/backend/pkg/middleware/security_test.go
T
marcandClaude Opus 5 9b8e82cdb2 feat(honeybar): HoneyOS-Leiste einbinden, CSP-Ausnahme dafür
Die Leiste kommt als Skript von der HoneyOS-Domain und holt sich von dort
ihre App-Liste. Beides blockte die CSP bisher (script-src / connect-src
fielen auf 'self' zurueck) – ohne Ausnahme erschien sie gar nicht.

- HONEYBAR_ORIGIN nimmt genau diesen einen Ursprung in script-src und
  connect-src auf. Leer heisst: keine Leiste, und die Regel bleibt so eng
  wie bisher. Frame-Freigabe erbt sie ausdruecklich nicht.
- SecurityHeaders nimmt die Ausnahmen jetzt als CSP-Struct statt als
  variadische frameSrc-Liste – mit drei Richtungen wurde das sonst unklar.
- Das <script>-Tag steht in der index.html und nicht serverseitig
  eingespritzt: der Service Worker legt die index.html in seinen Vorrat
  (navigateFallback), was zur Bauzeit fehlt, kaeme im Browser nie an.
- defer, damit ein langsames oder abgeschaltetes HoneyOS den Start von
  Aether nicht ausbremst.

Tests fuer die neue Regel: eng ohne Ausnahme, beide Richtungen mit,
Pfad wird auf den Ursprung gekuerzt, Adresse ohne Schema faellt raus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 21:03:01 +02:00

84 lines
2.7 KiB
Go

package middleware
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func policyFor(t *testing.T, csp CSP) string {
t.Helper()
h := SecurityHeaders(csp)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
return rec.Header().Get("Content-Security-Policy")
}
// Ohne Ausnahmen darf nichts Fremdes erlaubt sein — das ist der Normalfall
// und der Zustand, in dem eine Instanz ohne Zusatzkonfiguration läuft.
func TestCSPOhneAusnahmenBleibtEng(t *testing.T) {
got := policyFor(t, CSP{})
for _, want := range []string{
"default-src 'self'",
"script-src 'self';",
"connect-src 'self';",
"frame-src 'self'",
} {
if !strings.Contains(got+";", want) {
t.Errorf("Regel %q fehlt in %q", want, got)
}
}
if strings.Contains(got, "*") {
t.Errorf("Platzhalter in der Regel: %q", got)
}
}
// Die eingebettete Leiste braucht zwei Richtungen: das Skript selbst und die
// Anfrage, mit der es sich seine Daten holt.
func TestCSPLeisteBekommtSkriptUndVerbindung(t *testing.T) {
const bar = "https://os.example.com"
got := policyFor(t, CSP{ScriptSrc: []string{bar}, ConnectSrc: []string{bar}})
if !strings.Contains(got, "script-src 'self' "+bar) {
t.Errorf("script-src erlaubt die Leiste nicht: %q", got)
}
if !strings.Contains(got, "connect-src 'self' "+bar) {
t.Errorf("connect-src erlaubt die Anfrage nicht: %q", got)
}
// Die Ausnahme gilt nur, wofür sie gedacht ist.
if strings.Contains(got, "frame-src 'self' "+bar) {
t.Errorf("die Leiste darf keine Rahmen-Freigabe erben: %q", got)
}
}
// Ein Pfad in der Konfiguration wäre enger als gemeint; ein Tippfehler ohne
// Schema soll gar nicht erst in die Regel geraten.
func TestCSPKürztAufDenUrsprungUndVerwirftUnsinn(t *testing.T) {
got := policyFor(t, CSP{ScriptSrc: []string{"https://os.example.com/honeybar/honeybar.js"}})
if !strings.Contains(got, "script-src 'self' https://os.example.com;") {
t.Errorf("Pfad nicht abgeschnitten: %q", got)
}
got = policyFor(t, CSP{ScriptSrc: []string{"os.example.com", "", " "}})
if !strings.Contains(got+";", "script-src 'self';") {
t.Errorf("Adresse ohne Schema wurde übernommen: %q", got)
}
}
func TestSicherheitsköpfeStehen(t *testing.T) {
h := SecurityHeaders(CSP{})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
for k, want := range map[string]string{
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
} {
if got := rec.Header().Get(k); got != want {
t.Errorf("%s = %q, want %q", k, got, want)
}
}
}