Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,39 @@ func (s *Service) Handlers() (authHandler, avatarHandler http.Handler) {
p.Handler(w, r)
}

return http.HandlerFunc(ah), http.HandlerFunc(s.avatarProxy.Handler)
return withSecurityHeaders(http.HandlerFunc(ah)), withSecurityHeaders(http.HandlerFunc(s.avatarProxy.Handler))
}

// withSecurityHeaders wraps an auth response handler to apply strict CSP and nosniff
// on every response. The go-pkgz/auth package's own response surface is JSON-only
// for auth routes and images for the avatar route — no built-in HTML rendering
// anywhere — so this CSP is unconditionally safe and gives the auth origin
// defense-in-depth against any future trust-boundary regression that might emit a
// renderable body.
//
// - Content-Security-Policy: default-src 'none'; sandbox — blocks inline scripts
// and event handlers even if a body is ever served as HTML by mistake; the
// sandbox directive additionally isolates any rendered document from this origin.
// - X-Content-Type-Options: nosniff — prevents browsers from MIME-overriding the
// declared Content-Type to a more dangerous one.
//
// The avatar Handler additionally sets Content-Disposition: inline; filename="avatar"
// inside itself, so direct callers (tests, custom mounts) still get the full header
// set without going through this wrapper.
//
// CONSUMER NOTE: custom providers added via Service.AddCustomHandler / AddProvider
// are also wrapped. If a custom provider renders HTML (login forms, JS-based flows,
// the dev_provider's login page, etc.), the strict CSP will block inline scripts and
// event handlers on those pages. Such providers should either (a) override the CSP
// for their own response by calling w.Header().Set("Content-Security-Policy", ...)
// before writing — Set replaces the wrapper's value — or (b) move any required
// scripts/styles to external files served from 'self'.
Comment on lines +262 to +265
func withSecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'none'; sandbox; frame-ancestors 'none'")
w.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(w, r)
})
}

// Middleware returns auth middleware
Expand Down
35 changes: 35 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,41 @@ func TestLogoutNoProviders(t *testing.T) {
assert.Equal(t, "{\"error\":\"providers not defined\"}\n", string(b))
}

// TestHandlers_SecurityHeaders proves the mandatory CSP + nosniff wrapper is applied
// to every response returned by Service.Handlers(), regardless of route or status.
// go-pkgz/auth has no customisable HTML rendering, so this CSP is unconditionally safe.
func TestHandlers_SecurityHeaders(t *testing.T) {
svc := NewService(Opts{Logger: logger.Std})
authRoute, _ := svc.Handlers()

mux := http.NewServeMux()
mux.Handle("/auth/", authRoute)
ts := httptest.NewServer(mux)
defer ts.Close()

// avatar handler verified separately in avatar_test.go; here we just need to
// confirm the wrapper applies across the auth route group.
probes := []string{
"/auth/list", // 200 JSON
"/auth/logout", // 400 JSON (no providers configured)
"/auth/user", // 401 JSON (no jwt)
"/auth/status", // 200 JSON
"/auth/bad/login", // 400 JSON
}
for _, path := range probes {
t.Run(path, func(t *testing.T) {
resp, err := http.Get(ts.URL + path)
require.NoError(t, err)
defer resp.Body.Close()
csp := resp.Header.Get("Content-Security-Policy")
assert.Contains(t, csp, "default-src 'none'", "missing strict CSP on %s (status=%d)", path, resp.StatusCode)
assert.Contains(t, csp, "sandbox", "missing sandbox directive on %s", path)
assert.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options"),
"missing nosniff on %s", path)
})
}
}

func TestBadRequests(t *testing.T) {
_, teardown := prepService(t)
defer teardown()
Expand Down
Loading
Loading