Merge branch 'develop' into TM-309

This commit is contained in:
Mazyar
2025-12-08 12:08:44 +03:30
12 changed files with 768 additions and 27 deletions
+144
View File
@@ -0,0 +1,144 @@
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)
}
}
}
// 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
}
+82
View File
@@ -0,0 +1,82 @@
package security
import (
"html"
"regexp"
"strings"
"github.com/microcosm-cc/bluemonday"
)
// XSSPolicy defines the sanitization policy
type XSSPolicy struct {
policy *bluemonday.Policy
}
// NewXSSPolicy creates a new XSS sanitization policy
func NewXSSPolicy() *XSSPolicy {
p := bluemonday.UGCPolicy()
// Allow basic formatting but strip scripts and dangerous attributes
p.AllowAttrs("href", "target", "rel").OnElements("a")
p.AllowAttrs("src", "alt", "width", "height").OnElements("img")
p.AllowElements("p", "br", "strong", "em", "u", "h1", "h2", "h3", "h4", "h5", "h6")
p.AllowElements("ul", "ol", "li")
return &XSSPolicy{policy: p}
}
// SanitizeHTML sanitizes HTML input, allowing safe tags
func (x *XSSPolicy) SanitizeHTML(input string) string {
return x.policy.Sanitize(input)
}
// SanitizeText completely escapes HTML entities for safe text display
func (x *XSSPolicy) SanitizeText(input string) string {
return html.EscapeString(input)
}
// SanitizeEmail sanitizes input for use in email contexts
func (x *XSSPolicy) SanitizeEmail(input string) string {
// For emails, we want to allow basic formatting but be strict
return strings.TrimSpace(x.SanitizeText(input))
}
// SanitizeURL sanitizes URL input
func (x *XSSPolicy) SanitizeURL(input string) string {
// Remove javascript: and data: schemes
dangerous := regexp.MustCompile(`(?i)^(javascript|data|vbscript):`)
if dangerous.MatchString(input) {
return ""
}
return strings.TrimSpace(input)
}
// SanitizePhoneNumber sanitizes phone number input
func (x *XSSPolicy) SanitizePhoneNumber(input string) string {
// Remove any HTML tags and trim whitespace
clean := regexp.MustCompile(`<[^>]*>`).ReplaceAllString(input, "")
return strings.TrimSpace(clean)
}
// ValidateAndSanitizeInput validates and sanitizes user input
func (x *XSSPolicy) ValidateAndSanitizeInput(input string, fieldType string) (string, error) {
if strings.TrimSpace(input) == "" {
return "", nil
}
switch fieldType {
case "text":
return x.SanitizeText(input), nil
case "html":
return x.SanitizeHTML(input), nil
case "email":
return x.SanitizeEmail(input), nil
case "url":
return x.SanitizeURL(input), nil
case "phone":
return x.SanitizePhoneNumber(input), nil
default:
return x.SanitizeText(input), nil
}
}