166 lines
4.8 KiB
Go
166 lines
4.8 KiB
Go
package security
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// CSPConfig holds CSP configuration
|
|
type CSPConfig struct {
|
|
DefaultSrc []string
|
|
ScriptSrc []string
|
|
StyleSrc []string
|
|
ImgSrc []string
|
|
ConnectSrc []string
|
|
FontSrc []string
|
|
ObjectSrc []string
|
|
MediaSrc []string
|
|
FrameSrc []string
|
|
ReportURI string
|
|
ReportOnly bool
|
|
}
|
|
|
|
// DefaultCSPConfig returns a secure default CSP configuration
|
|
func DefaultCSPConfig() *CSPConfig {
|
|
return &CSPConfig{
|
|
DefaultSrc: []string{"'self'"},
|
|
ScriptSrc: []string{"'self'"},
|
|
StyleSrc: []string{"'self'"},
|
|
ImgSrc: []string{"'self'", "data:", "https:", "http:"},
|
|
ConnectSrc: []string{"'self'", "https://api.opplens.com"},
|
|
FontSrc: []string{"'self'", "https://fonts.gstatic.com"},
|
|
ObjectSrc: []string{"'none'"},
|
|
MediaSrc: []string{"'self'"},
|
|
FrameSrc: []string{"'none'"},
|
|
ReportURI: "/api/v1/csp-report",
|
|
ReportOnly: false,
|
|
}
|
|
}
|
|
|
|
// CSPSource represents a CSP directive source
|
|
type CSPSource string
|
|
|
|
// CSPDirective represents a CSP directive
|
|
type CSPDirective struct {
|
|
Name string
|
|
Sources []CSPSource
|
|
}
|
|
|
|
// CSPMiddleware creates Echo middleware for Content Security Policy
|
|
func CSPMiddleware(config *CSPConfig) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
// Build CSP header
|
|
directives := []string{}
|
|
|
|
if len(config.DefaultSrc) > 0 {
|
|
directives = append(directives, "default-src "+strings.Join(toStringSlice(config.DefaultSrc), " "))
|
|
}
|
|
|
|
if len(config.ScriptSrc) > 0 {
|
|
directives = append(directives, "script-src "+strings.Join(toStringSlice(config.ScriptSrc), " "))
|
|
}
|
|
|
|
if len(config.StyleSrc) > 0 {
|
|
directives = append(directives, "style-src "+strings.Join(toStringSlice(config.StyleSrc), " "))
|
|
}
|
|
|
|
if len(config.ImgSrc) > 0 {
|
|
directives = append(directives, "img-src "+strings.Join(toStringSlice(config.ImgSrc), " "))
|
|
}
|
|
|
|
if len(config.ConnectSrc) > 0 {
|
|
directives = append(directives, "connect-src "+strings.Join(toStringSlice(config.ConnectSrc), " "))
|
|
}
|
|
|
|
if len(config.FontSrc) > 0 {
|
|
directives = append(directives, "font-src "+strings.Join(toStringSlice(config.FontSrc), " "))
|
|
}
|
|
|
|
if len(config.ObjectSrc) > 0 {
|
|
directives = append(directives, "object-src "+strings.Join(toStringSlice(config.ObjectSrc), " "))
|
|
}
|
|
|
|
if len(config.MediaSrc) > 0 {
|
|
directives = append(directives, "media-src "+strings.Join(toStringSlice(config.MediaSrc), " "))
|
|
}
|
|
|
|
if len(config.FrameSrc) > 0 {
|
|
directives = append(directives, "frame-src "+strings.Join(toStringSlice(config.FrameSrc), " "))
|
|
}
|
|
|
|
if config.ReportURI != "" {
|
|
directives = append(directives, "report-uri "+config.ReportURI)
|
|
}
|
|
|
|
cspHeader := strings.Join(directives, "; ")
|
|
|
|
if config.ReportOnly {
|
|
c.Response().Header().Set("Content-Security-Policy-Report-Only", cspHeader)
|
|
} else {
|
|
c.Response().Header().Set("Content-Security-Policy", cspHeader)
|
|
}
|
|
|
|
return next(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// CSPMiddlewareForPaths applies CSP middleware based on request path prefix.
|
|
// Requests matching no prefix proceed without a CSP header.
|
|
func CSPMiddlewareForPaths(adminPrefix string, adminConfig *CSPConfig, publicPrefix string, publicConfig *CSPConfig) echo.MiddlewareFunc {
|
|
adminMW := CSPMiddleware(adminConfig)
|
|
publicMW := CSPMiddleware(publicConfig)
|
|
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
path := c.Request().URL.Path
|
|
switch {
|
|
case strings.HasPrefix(path, adminPrefix):
|
|
return adminMW(next)(c)
|
|
case strings.HasPrefix(path, publicPrefix):
|
|
return publicMW(next)(c)
|
|
default:
|
|
return next(c)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// CSPReportHandler handles CSP violation reports
|
|
func CSPReportHandler(c echo.Context) error {
|
|
var report struct {
|
|
CSPReport struct {
|
|
DocumentURI string `json:"document-uri"`
|
|
ViolatedDirective string `json:"violated-directive"`
|
|
EffectiveDirective string `json:"effective-directive"`
|
|
OriginalPolicy string `json:"original-policy"`
|
|
BlockedURI string `json:"blocked-uri"`
|
|
StatusCode int `json:"status-code"`
|
|
} `json:"csp-report"`
|
|
}
|
|
|
|
if err := c.Bind(&report); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid CSP report"})
|
|
}
|
|
|
|
// Log CSP violation (implement proper logging here)
|
|
// logger.Warn("CSP Violation", map[string]interface{}{
|
|
// "document_uri": report.CSPReport.DocumentURI,
|
|
// "violated_directive": report.CSPReport.ViolatedDirective,
|
|
// "blocked_uri": report.CSPReport.BlockedURI,
|
|
// })
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{"status": "reported"})
|
|
}
|
|
|
|
func toStringSlice(sources []string) []string {
|
|
result := make([]string, len(sources))
|
|
for i, src := range sources {
|
|
result[i] = string(src)
|
|
}
|
|
return result
|
|
}
|