Compare commits
2
Commits
556d0fa2e1
...
44fe3c5772
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44fe3c5772 | ||
|
|
e08cafe9c8 |
@@ -60,6 +60,7 @@ func (a *apiServer) routes() http.Handler {
|
|||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
r.Use(middleware.Timeout(30 * time.Second))
|
r.Use(middleware.Timeout(30 * time.Second))
|
||||||
r.Use(viewerCookieMiddleware)
|
r.Use(viewerCookieMiddleware)
|
||||||
|
r.Use(web.SecurityHeaders) // #59: CSP + hardening headers on HTML pages
|
||||||
|
|
||||||
// admin (#40): HTML page is open (key entry via form); API is key-guarded
|
// admin (#40): HTML page is open (key entry via form); API is key-guarded
|
||||||
r.Get("/admin", a.ui.Handlers().HandleAdminPage)
|
r.Get("/admin", a.ui.Handlers().HandleAdminPage)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// #59: SecurityHeaders must add the CSP and hardening headers to rendered
|
||||||
|
// HTML responses only; JSON and /raw responses pass through untouched.
|
||||||
|
func TestSecurityHeaders(t *testing.T) {
|
||||||
|
pages := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Write([]byte("<html><body>ok</body></html>"))
|
||||||
|
})
|
||||||
|
h := SecurityHeaders(pages)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
|
||||||
|
wantCSP := "default-src 'self'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'"
|
||||||
|
if got := rec.Header().Get("Content-Security-Policy"); got != wantCSP {
|
||||||
|
t.Errorf("CSP = %q, want %q", got, wantCSP)
|
||||||
|
}
|
||||||
|
if got := rec.Header().Get("Referrer-Policy"); got != "no-referrer" {
|
||||||
|
t.Errorf("Referrer-Policy = %q, want no-referrer", got)
|
||||||
|
}
|
||||||
|
if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" {
|
||||||
|
t.Errorf("X-Content-Type-Options = %q, want nosniff", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON response: no security headers.
|
||||||
|
jsonh := SecurityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(`{"ok":true}`))
|
||||||
|
}))
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
jsonh.ServeHTTP(rec, httptest.NewRequest("GET", "/api/x", nil))
|
||||||
|
if got := rec.Header().Get("Content-Security-Policy"); got != "" {
|
||||||
|
t.Errorf("unexpected CSP %q on JSON response", got)
|
||||||
|
}
|
||||||
|
if got := rec.Header().Get("Referrer-Policy"); got != "" {
|
||||||
|
t.Errorf("unexpected Referrer-Policy %q on JSON response", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content type set after the first Write (as the inline can page does) is
|
||||||
|
// still picked up because headers are inspected post-handler.
|
||||||
|
lateh := SecurityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("<html></html>"))
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
}))
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
lateh.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
|
||||||
|
if got := rec.Header().Get("Content-Security-Policy"); !strings.Contains(got, "frame-ancestors 'none'") {
|
||||||
|
t.Errorf("CSP = %q, want frame-ancestors 'none'", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -294,3 +294,23 @@ func (h *Handlers) HandleAdminPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Handlers builds a web.Handlers bound to this UI.
|
// Handlers builds a web.Handlers bound to this UI.
|
||||||
func (u *UI) Handlers() *Handlers { return &Handlers{UI: u} }
|
func (u *UI) Handlers() *Handlers { return &Handlers{UI: u} }
|
||||||
|
|
||||||
|
// #59: security headers for rendered HTML pages. Applied wherever the
|
||||||
|
// response is text/html (page templates and the inline can page); JSON API
|
||||||
|
// responses and /raw content pass through untouched. script-src allows
|
||||||
|
// 'unsafe-inline' because the page templates carry inline scripts; CSP
|
||||||
|
// default-src 'self' still blocks external content and object/frame embeds,
|
||||||
|
// and frame-ancestors 'none' closes the clickjacking gap flagged in the #34
|
||||||
|
// pentest. Runs after the handler so the Content-Type is already set.
|
||||||
|
func SecurityHeaders(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
h := w.Header()
|
||||||
|
if strings.HasPrefix(h.Get("Content-Type"), "text/html") {
|
||||||
|
h.Set("Content-Security-Policy",
|
||||||
|
"default-src 'self'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'")
|
||||||
|
h.Set("Referrer-Policy", "no-referrer")
|
||||||
|
h.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user