Files
warmbly/internal/api/middleware/security_headers_test.go
T
Matthew MeszarosandDevin AI 416c4feee3 feat: prevent private API response storage and remove wildcard CORS from the static Pages dashboard
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-06 12:05:54 +00:00

109 lines
3.6 KiB
Go

package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestSecurityHeadersAreSet(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(SecurityHeaders())
r.GET("/x", func(c *gin.Context) { c.Status(http.StatusOK) })
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/x", nil))
want := map[string]string{
"Cache-Control": "no-store",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "same-site",
}
for k, v := range want {
if got := w.Header().Get(k); got != v {
t.Errorf("%s = %q, want %q", k, got, v)
}
}
if w.Header().Get("Content-Security-Policy") == "" {
t.Error("a default Content-Security-Policy should be set")
}
}
func TestSecurityHeadersPreventCachingErrors(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, status := range []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusInternalServerError} {
t.Run(http.StatusText(status), func(t *testing.T) {
r := gin.New()
r.Use(SecurityHeaders())
r.GET("/x", func(c *gin.Context) { c.AbortWithStatus(status) })
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/x", nil))
if got := w.Header().Get("Cache-Control"); got != "no-store" {
t.Errorf("Cache-Control = %q, want no-store", got)
}
})
}
}
func TestHandlerPublicCachePolicyWins(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(SecurityHeaders())
r.GET("/public/image.png", func(c *gin.Context) {
c.Header("Cache-Control", "public, max-age=31536000, immutable")
c.Status(http.StatusOK)
})
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/public/image.png", nil))
if got := w.Header().Get("Cache-Control"); got != "public, max-age=31536000, immutable" {
t.Errorf("handler cache policy should survive, got %q", got)
}
}
// HSTS over plain HTTP is ignored by browsers and would pin a self-hosted LAN
// instance to a scheme it does not serve, so it is only sent on a TLS request.
func TestHSTSOnlyOverTLS(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(SecurityHeaders())
r.GET("/x", func(c *gin.Context) { c.Status(http.StatusOK) })
plain := httptest.NewRecorder()
r.ServeHTTP(plain, httptest.NewRequest(http.MethodGet, "/x", nil))
if got := plain.Header().Get("Strict-Transport-Security"); got != "" {
t.Errorf("no HSTS expected over plain HTTP, got %q", got)
}
forwarded := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/x", nil)
req.Header.Set("X-Forwarded-Proto", "https")
r.ServeHTTP(forwarded, req)
if got := forwarded.Header().Get("Strict-Transport-Security"); got == "" {
t.Error("HSTS expected when the edge reports https")
}
}
// A handler that sets its own policy keeps it: the OAuth bouncer pages need
// script-src 'unsafe-inline' for the one script that is the page.
func TestHandlerCSPWins(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(SecurityHeaders())
r.GET("/page", func(c *gin.Context) {
c.Header("Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'")
c.Status(http.StatusOK)
})
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/page", nil))
if got := w.Header().Get("Content-Security-Policy"); got != "default-src 'none'; script-src 'unsafe-inline'" {
t.Errorf("handler policy should survive, got %q", got)
}
}