mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-10-23 13:32:53 +00:00
More security headers and checks
This commit is contained in:
1 parent
b0683af5a0
commit
01c7adffdd
4 files changed
+75
-6
No files matched your search
@@ -8,6 +8,7 @@ when:
|
||||
depends_on:
|
||||
- test
|
||||
- lint
|
||||
- vuln
|
||||
|
||||
steps:
|
||||
publish:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Scans dependencies and the Go stdlib for known, reachable vulnerabilities
|
||||
# (govulncheck uses call-graph analysis, so it flags CVEs the code actually
|
||||
# reaches, not just present-in-go.sum). Runs on every push and PR; the build
|
||||
# depends on it, so a reachable advisory blocks the image build and deploy.
|
||||
# Tracks the tool's latest, matching lint.yaml's :latest philosophy; the vuln DB
|
||||
# is fetched at run.
|
||||
when:
|
||||
- event: [push, pull_request]
|
||||
|
||||
steps:
|
||||
govulncheck:
|
||||
image: golang:1.26.4
|
||||
environment:
|
||||
# go.mod may target a newer Go than the image bundles; fetch on demand.
|
||||
GOTOOLCHAIN: auto
|
||||
commands:
|
||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- govulncheck ./...
|
||||
|
||||
depends_on: []
|
||||
@@ -29,16 +29,32 @@ var cspDirectives = strings.Join([]string{
|
||||
"object-src 'none'",
|
||||
}, "; ")
|
||||
|
||||
// permissionsPolicy denies powerful browser features we don't use and explicitly
|
||||
// allows the two we do: WebSerial (the KISS modem on the confirm/console pages)
|
||||
// and WebAuthn (passkeys). Unknown tokens are ignored by browsers that don't
|
||||
// implement them.
|
||||
const permissionsPolicy = "serial=(self), " +
|
||||
"publickey-credentials-get=(self), publickey-credentials-create=(self), " +
|
||||
"geolocation=(), camera=(), microphone=(), payment=(), usb=()"
|
||||
|
||||
// securityHeaders sets a strict Content-Security-Policy (with a fresh per-request
|
||||
// script nonce, also exposed via the context for templates) plus a couple of
|
||||
// companion hardening headers, on every response.
|
||||
// script nonce, also exposed via the context for templates) plus companion
|
||||
// hardening headers, on every response.
|
||||
func (e *Env) securityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nonce := newNonce()
|
||||
w.Header().Set("Content-Security-Policy",
|
||||
cspDirectives+"; script-src 'self' 'nonce-"+nonce+"'")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
h := w.Header()
|
||||
h.Set("Content-Security-Policy", cspDirectives+"; script-src 'self' 'nonce-"+nonce+"'")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
h.Set("X-Frame-Options", "DENY") // old-browser fallback for frame-ancestors 'none'
|
||||
h.Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
h.Set("Permissions-Policy", permissionsPolicy)
|
||||
// HSTS only over TLS — never on plain-http localhost dev, where it would
|
||||
// pin the browser to https for the dev host.
|
||||
if e.Cfg != nil && e.Cfg.Secure {
|
||||
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), nonceCtxKey{}, nonce)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jleight/meshtender/internal/config"
|
||||
)
|
||||
|
||||
func TestSecurityHeadersCSPNonce(t *testing.T) {
|
||||
@@ -33,11 +35,41 @@ func TestSecurityHeadersCSPNonce(t *testing.T) {
|
||||
if rec1.Header().Get("X-Content-Type-Options") != "nosniff" {
|
||||
t.Errorf("missing X-Content-Type-Options: nosniff")
|
||||
}
|
||||
if rec1.Header().Get("X-Frame-Options") != "DENY" {
|
||||
t.Errorf("missing X-Frame-Options: DENY")
|
||||
}
|
||||
if rec1.Header().Get("Cross-Origin-Opener-Policy") != "same-origin" {
|
||||
t.Errorf("missing Cross-Origin-Opener-Policy: same-origin")
|
||||
}
|
||||
// Permissions-Policy must keep the features we actually use.
|
||||
pp := rec1.Header().Get("Permissions-Policy")
|
||||
if !strings.Contains(pp, "serial=(self)") || !strings.Contains(pp, "publickey-credentials-get=(self)") {
|
||||
t.Errorf("Permissions-Policy doesn't allow serial/webauthn: %q", pp)
|
||||
}
|
||||
if seen[0] == "" || seen[1] == "" || seen[0] == seen[1] {
|
||||
t.Errorf("nonce not fresh per request: %q, %q", seen[0], seen[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityHeadersHSTSGatedOnTLS(t *testing.T) {
|
||||
t.Parallel()
|
||||
// No TLS (nil/insecure config): no HSTS.
|
||||
rec := httptest.NewRecorder()
|
||||
(&Env{}).securityHeaders(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})).
|
||||
ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
if rec.Header().Get("Strict-Transport-Security") != "" {
|
||||
t.Errorf("HSTS set without TLS: %q", rec.Header().Get("Strict-Transport-Security"))
|
||||
}
|
||||
// TLS on: HSTS present.
|
||||
rec = httptest.NewRecorder()
|
||||
(&Env{Cfg: &config.Config{Secure: true}}).
|
||||
securityHeaders(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})).
|
||||
ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
if !strings.HasPrefix(rec.Header().Get("Strict-Transport-Security"), "max-age=") {
|
||||
t.Errorf("HSTS missing under TLS: %q", rec.Header().Get("Strict-Transport-Security"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplatesHaveNoInlineJS enforces the CSP-compatible pattern across every
|
||||
// template: no inline event handlers (on*=), and no un-nonce'd inline <script>.
|
||||
// Nonces don't cover on*= handlers, so any new one would silently break under CSP.
|
||||
|
||||
Reference in new issue
Block a user