aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/web.go18
-rw-r--r--web/web_test.go25
2 files changed, 42 insertions, 1 deletions
diff --git a/web/web.go b/web/web.go
index 4868577..11a5831 100644
--- a/web/web.go
+++ b/web/web.go
@@ -259,7 +259,7 @@ func NewRouter(cfg *config.Settings) http.Handler {
mux.Handle("/", GzipMiddleware(AuthWrap(http.HandlerFunc(indexHandler))))
- return CSRFMiddleware(mux)
+ return SecurityHeadersMiddleware(CSRFMiddleware(mux))
}
func Serve(cfg *config.Settings) {
@@ -383,3 +383,19 @@ func CSRFMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
})
}
+
+func SecurityHeadersMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+ w.Header().Set("X-Frame-Options", "DENY")
+ w.Header().Set("X-XSS-Protection", "1; mode=block")
+ w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
+ // Content-Security-Policy
+ // default-src 'self'
+ // style-src 'self' 'unsafe-inline' (for React/styled-components if used)
+ // img-src 'self' data: * (RSS images can be from anywhere)
+ // connect-src 'self' (API calls)
+ w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: *; connect-src 'self'; frame-ancestors 'none';")
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/web/web_test.go b/web/web_test.go
index 89ca998..c6cf306 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -10,10 +10,15 @@ import (
"adammathes.com/neko/api"
"adammathes.com/neko/config"
+ "adammathes.com/neko/internal/safehttp"
"adammathes.com/neko/models"
"golang.org/x/crypto/bcrypt"
)
+func init() {
+ safehttp.AllowLocal = true
+}
+
func setupTestDB(t *testing.T) {
t.Helper()
config.Config.DBFile = filepath.Join(t.TempDir(), "test.db")
@@ -774,3 +779,23 @@ func TestCSRFMiddleware(t *testing.T) {
t.Errorf("Expected 200 for POST with valid token, got %d", rr.Code)
}
}
+
+func TestSecurityHeadersMiddleware(t *testing.T) {
+ handler := SecurityHeadersMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if rr.Header().Get("X-Content-Type-Options") != "nosniff" {
+ t.Error("Missing X-Content-Type-Options: nosniff")
+ }
+ if rr.Header().Get("X-Frame-Options") != "DENY" {
+ t.Error("Missing X-Frame-Options: DENY")
+ }
+ if rr.Header().Get("Content-Security-Policy") == "" {
+ t.Error("Missing Content-Security-Policy")
+ }
+}