xss validation
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user