package middleware
import (
"net/http"
"net/http/httptest"
"strings"
"github.com/labstack/echo/v4"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("SecurityHeaders", func() {
var e *echo.Echo
BeforeEach(func() {
e = echo.New()
e.Use(SecurityHeaders())
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "ok")
})
})
It("sets Content-Security-Policy", func() {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
Expect(rec.Code).To(Equal(http.StatusOK))
csp := rec.Header().Get("Content-Security-Policy")
Expect(csp).ToNot(BeEmpty())
Expect(csp).To(ContainSubstring("default-src 'self'"))
Expect(csp).To(ContainSubstring("frame-ancestors 'self'"))
Expect(csp).To(ContainSubstring("object-src 'none'"))
Expect(csp).To(ContainSubstring("base-uri 'self'"))
// blob: must be in connect-src so the waveform renderer can XHR/fetch
// a freshly-created object URL (uploaded/enhanced clip).
Expect(csp).To(ContainSubstring("connect-src 'self' ws: wss: https: blob:"))
})
It("sets X-Content-Type-Options: nosniff", func() {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
Expect(rec.Header().Get("X-Content-Type-Options")).To(Equal("nosniff"))
})
It("sets X-Frame-Options: SAMEORIGIN", func() {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
Expect(rec.Header().Get("X-Frame-Options")).To(Equal("SAMEORIGIN"))
})
It("sets Referrer-Policy", func() {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
Expect(rec.Header().Get("Referrer-Policy")).To(Equal("strict-origin-when-cross-origin"))
})
It("does not overwrite a header a later handler set explicitly", func() {
// Reset router so we can install a handler that sets CSP itself.
e = echo.New()
e.Use(SecurityHeaders())
e.GET("/", func(c echo.Context) error {
c.Response().Header().Set("Content-Security-Policy", "default-src 'none'")
return c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// The middleware runs first (sets default), but a later handler may
// want a tighter CSP for a specific response. The middleware should
// only set headers that aren't already present — but since Echo
// middleware runs around the handler, the middleware's Set calls
// happen before the handler runs. So this is more of a smoke test
// that the middleware doesn't actively clobber on the way out.
Expect(rec.Header().Get("Content-Security-Policy")).To(Equal("default-src 'none'"))
})
})
var _ = Describe("SecureBaseHref", func() {
It("escapes attribute-breaking characters", func() {
out := SecureBaseHref(`">`)
Expect(out).ToNot(ContainSubstring(`"`))
Expect(out).ToNot(ContainSubstring("<"))
Expect(out).ToNot(ContainSubstring(">"))
Expect(out).To(ContainSubstring("script"))
})
It("escapes ampersands", func() {
Expect(SecureBaseHref("https://example.com/?a=1&b=2")).
To(Equal("https://example.com/?a=1&b=2"))
})
It("escapes single quotes", func() {
Expect(SecureBaseHref(`x' onload='alert(1)`)).
To(ContainSubstring("'"))
})
It("leaves benign URLs alone", func() {
Expect(SecureBaseHref("https://example.com/app/")).
To(Equal("https://example.com/app/"))
})
It("encloses safely inside double-quoted attribute", func() {
// The realistic attack: attacker sets X-Forwarded-Host: foo.com" onload="x.
// Confirm the escaped form can't break out of the surrounding quotes.
hostile := `foo.com" onload="alert(1)`
out := SecureBaseHref(hostile)
Expect(out).ToNot(ContainSubstring(`"`))
// Wrapped in attribute context — no raw quote means no breakout.
full := ``
Expect(strings.Count(full, `"`)).To(Equal(2))
})
})