package middleware import ( "regexp" "strings" "github.com/gofiber/fiber/v3" ) // SecurityHeaders sets CSP and other security response headers. // Per review-resolution #27: CSP tightened to script-src 'self' (no unsafe-eval). func SecurityHeaders() fiber.Handler { return func(c fiber.Ctx) error { c.Set("Content-Security-Policy", "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; "+ "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; "+ "font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'") c.Set("Referrer-Policy", "no-referrer") c.Set("X-Content-Type-Options", "nosniff") c.Set("X-Frame-Options", "DENY") c.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()") return c.Next() } } // BodyLimit middleware. func BodyLimit(limit int) fiber.Handler { return func(c fiber.Ctx) error { if len(c.Body()) > limit { return c.Status(413).JSON(fiber.Map{ "status": "failed", "error": fiber.Map{"code": 413, "message": "Request body is too large"}, }) } return c.Next() } } // DownloadHostIsolation ensures download hosts only serve /download/ paths. func DownloadHostIsolation(downloadHosts []string) fiber.Handler { hostSet := make(map[string]bool, len(downloadHosts)) for _, h := range downloadHosts { h = strings.ToLower(strings.TrimSpace(h)) if h != "" { hostSet[h] = true } } return func(c fiber.Ctx) error { if len(hostSet) == 0 { return c.Next() } host := strings.ToLower(c.Hostname()) if hostSet[host] && !strings.HasPrefix(c.Path(), "/download/") { return c.Status(404).SendString("Not Found") } return c.Next() } } // crlfRe matches CR and LF characters for injection prevention. var crlfRe = regexp.MustCompile(`[\r\n]`) // SetSafeResponseHeader sets a header value only if it doesn't contain CRLF. // Per review-resolution #25: CRLF injection protection. func SetSafeResponseHeader(c fiber.Ctx, name, value string) { if value != "" && !crlfRe.MatchString(value) { c.Set(name, value) } } // SafeContentDisposition sanitizes a filename for Content-Disposition. // Per review-resolution #25: strip CRLF and control chars, escape special chars. func SafeContentDisposition(value string) string { if value == "" || crlfRe.MatchString(value) { return "" } // Extract filename from the header value filename := extractFilename(value) if filename == "" { return "" } // Sanitize: keep alphanumerics, dots, hyphens, parens, CJK, spaces safe := sanitizeFilename(filename) if safe == "" { return "" } return `attachment; filename="` + safe + `"` } var filenameRe = regexp.MustCompile(`filename\*?=(?:UTF-8''|")?([^";]+)`) func extractFilename(value string) string { matches := filenameRe.FindStringSubmatch(value) if len(matches) < 2 { return "" } return strings.TrimSpace(matches[1]) } var sanitizeRe = regexp.MustCompile(`[^a-zA-Z0-9.()\- \x{4e00}-\x{9fff}]`) func sanitizeFilename(name string) string { safe := sanitizeRe.ReplaceAllString(name, "_") if len(safe) > 120 { safe = safe[:120] } return safe }