script-src now permits 'unsafe-inline' and https://unpkg.com so the autocomplete script and HTMX library load correctly.
92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
// kafka — a privacy-respecting metasearch engine
|
|
// Copyright (C) 2026-present metamorphosis-dev
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// SecurityHeadersConfig controls which security headers are set.
|
|
type SecurityHeadersConfig struct {
|
|
// FrameOptions controls X-Frame-Options. Default: "DENY".
|
|
FrameOptions string
|
|
// HSTSMaxAge controls the max-age for Strict-Transport-Security.
|
|
// Set to 0 to disable HSTS (useful for local dev). Default: 31536000 (1 year).
|
|
HSTSMaxAge int
|
|
// HSTSPreloadDomains adds "includeSubDomains; preload" to HSTS.
|
|
HSTSPreloadDomains bool
|
|
// ReferrerPolicy controls the Referrer-Policy header. Default: "no-referrer".
|
|
ReferrerPolicy string
|
|
// CSP controls Content-Security-Policy. Default: a restrictive policy.
|
|
// Set to "" to disable CSP entirely.
|
|
CSP string
|
|
}
|
|
|
|
// SecurityHeaders returns middleware that sets standard HTTP security headers
|
|
// on every response.
|
|
func SecurityHeaders(cfg SecurityHeadersConfig) func(http.Handler) http.Handler {
|
|
frameOpts := cfg.FrameOptions
|
|
if frameOpts == "" {
|
|
frameOpts = "DENY"
|
|
}
|
|
|
|
hstsAge := cfg.HSTSMaxAge
|
|
if hstsAge == 0 {
|
|
hstsAge = 31536000 // 1 year
|
|
}
|
|
|
|
refPol := cfg.ReferrerPolicy
|
|
if refPol == "" {
|
|
refPol = "no-referrer"
|
|
}
|
|
|
|
csp := cfg.CSP
|
|
if csp == "" {
|
|
csp = defaultCSP()
|
|
}
|
|
|
|
hstsValue := "max-age=" + strconv.Itoa(hstsAge)
|
|
if cfg.HSTSPreloadDomains {
|
|
hstsValue += "; includeSubDomains; preload"
|
|
}
|
|
|
|
return func(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", frameOpts)
|
|
w.Header().Set("Referrer-Policy", refPol)
|
|
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
|
|
w.Header().Set("Content-Security-Policy", csp)
|
|
|
|
if hstsAge > 0 {
|
|
w.Header().Set("Strict-Transport-Security", hstsValue)
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// defaultCSP returns a restrictive Content-Security-Policy for the
|
|
// metasearch engine.
|
|
func defaultCSP() string {
|
|
return strings.Join([]string{
|
|
"default-src 'self'",
|
|
"script-src 'self' 'unsafe-inline' https://unpkg.com",
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' https: data:",
|
|
"connect-src 'self'",
|
|
"font-src 'self'",
|
|
"frame-ancestors 'none'",
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
}, "; ")
|
|
}
|