feat(server): security headers and a hash-based CSP for the web app (M462 #4980)
test-web / test (push) Successful in 55s
test-go / test (push) Successful in 1m14s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build signed APK (releases and dev) (push) Canceled after 2m50s
test-go / integration (push) Canceled after 2m50s
test-web / test (push) Successful in 55s
test-go / test (push) Successful in 1m14s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build signed APK (releases and dev) (push) Canceled after 2m50s
test-go / integration (push) Canceled after 2m50s
There were no security headers at all. Now: - every response: nosniff, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy (no camera/mic/geolocation), X-Frame-Options DENY; each set only if the handler hasn't. - HSTS only when the trusted proxy reports HTTPS (rule 94); never a redirect. - index.html carries a Content-Security-Policy whose script-src is 'self' plus the sha256 of each inline script in the page as served, computed after the branding template runs. No 'unsafe-inline' or 'unsafe-eval' for scripts. img-src admits remote https/http because Lidarr suggestion art is a remote poster URL. Hashing in Go rather than via SvelteKit's kit.csp covers the inline scripts SvelteKit doesn't know about (app.html's theme bootstrap and the branding global injected at build) and stays correct whatever the app name is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func hashOf(body string) string {
|
||||
sum := sha256.Sum256([]byte(body))
|
||||
return "'sha256-" + base64.StdEncoding.EncodeToString(sum[:]) + "'"
|
||||
}
|
||||
|
||||
func TestContentSecurityPolicy_HashesInlineScriptsOnly(t *testing.T) {
|
||||
theme := "(function () { document.documentElement.dataset.theme = 'dark'; })();"
|
||||
brand := `window.__MINSTREL__ = { appName: "Minstrel" };`
|
||||
html := "<html><head><script>" + theme + "</script>" +
|
||||
`<script type="module" src="/_app/start.js"></script>` +
|
||||
"<script>" + brand + "</script></head></html>"
|
||||
|
||||
csp := contentSecurityPolicy([]byte(html))
|
||||
|
||||
var scriptSrc string
|
||||
for _, d := range strings.Split(csp, "; ") {
|
||||
if strings.HasPrefix(d, "script-src ") {
|
||||
scriptSrc = d
|
||||
}
|
||||
}
|
||||
if scriptSrc == "" {
|
||||
t.Fatalf("no script-src in %q", csp)
|
||||
}
|
||||
for _, want := range []string{"'self'", hashOf(theme), hashOf(brand)} {
|
||||
if !strings.Contains(scriptSrc, want) {
|
||||
t.Errorf("script-src %q missing %s", scriptSrc, want)
|
||||
}
|
||||
}
|
||||
if strings.Count(scriptSrc, "'sha256-") != 2 {
|
||||
t.Errorf("script-src %q: want exactly the two inline scripts hashed, not the src= one", scriptSrc)
|
||||
}
|
||||
if strings.Contains(scriptSrc, "unsafe-inline") || strings.Contains(scriptSrc, "unsafe-eval") {
|
||||
t.Errorf("script-src must not fall back to unsafe-*: %q", scriptSrc)
|
||||
}
|
||||
for _, want := range []string{"frame-ancestors 'none'", "object-src 'none'", "connect-src 'self'"} {
|
||||
if !strings.Contains(csp, want) {
|
||||
t.Errorf("csp missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The hash must be of the page as served, after the branding template has
|
||||
// substituted the operator's app name, or a renamed instance would refuse
|
||||
// its own bootstrap script.
|
||||
func TestContentSecurityPolicy_TracksTemplatedContent(t *testing.T) {
|
||||
a := contentSecurityPolicy([]byte(`<script>window.__MINSTREL__ = { appName: "A" };</script>`))
|
||||
b := contentSecurityPolicy([]byte(`<script>window.__MINSTREL__ = { appName: "B" };</script>`))
|
||||
if a == b {
|
||||
t.Error("different script bodies produced the same policy")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user