Files
atlas/internal/httpapp/security_middleware.go

124 lines
4.1 KiB
Go

package httpapp
import (
"net/http"
"strings"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/errors"
)
// securityHeadersMiddleware adds security headers to responses
func (a *App) securityHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Security headers
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")
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
// HSTS (only for HTTPS)
if r.TLS != nil {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
// Content Security Policy (CSP)
// Allow Tailwind CDN, unpkg (for htmx), and jsdelivr for external resources
// Note: Tailwind CDN needs connect-src to fetch config and make network requests
csp := "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; connect-src 'self' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net;"
w.Header().Set("Content-Security-Policy", csp)
next.ServeHTTP(w, r)
})
}
// corsMiddleware handles CORS requests
func (a *App) corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Allow specific origins or all (for development)
allowedOrigins := []string{
"http://localhost:8080",
"http://localhost:3000",
"http://127.0.0.1:8080",
}
// Check if origin is allowed
allowed := false
for _, allowedOrigin := range allowedOrigins {
if origin == allowedOrigin {
allowed = true
break
}
}
// Allow requests from same origin
if origin == "" || r.Header.Get("Referer") != "" {
allowed = true
}
if allowed && origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "3600")
}
// Handle preflight requests
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// requestSizeMiddleware limits request body size
func (a *App) requestSizeMiddleware(maxSize int64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Limit request body size
r.Body = http.MaxBytesReader(w, r.Body, maxSize)
next.ServeHTTP(w, r)
})
}
}
// validateContentTypeMiddleware validates Content-Type for POST/PUT/PATCH requests
func (a *App) validateContentTypeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip for GET, HEAD, OPTIONS, DELETE
if r.Method == http.MethodGet || r.Method == http.MethodHead ||
r.Method == http.MethodOptions || r.Method == http.MethodDelete {
next.ServeHTTP(w, r)
return
}
// Skip for public endpoints
if a.isPublicEndpoint(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
// Check Content-Type for POST/PUT/PATCH
contentType := r.Header.Get("Content-Type")
if contentType == "" {
writeError(w, errors.ErrBadRequest("Content-Type header is required"))
return
}
// Allow JSON and form data
if !strings.HasPrefix(contentType, "application/json") &&
!strings.HasPrefix(contentType, "application/x-www-form-urlencoded") &&
!strings.HasPrefix(contentType, "multipart/form-data") {
writeError(w, errors.ErrBadRequest("Content-Type must be application/json"))
return
}
next.ServeHTTP(w, r)
})
}