xss validation

This commit is contained in:
Mazyar
2025-12-07 23:52:17 +03:30
parent b8a10f355e
commit 7fb182f180
10 changed files with 372 additions and 29 deletions
+6 -4
View File
@@ -107,6 +107,7 @@ import (
"tm/internal/tender"
"tm/internal/tender_approval"
"tm/internal/user"
"tm/pkg/security"
"tm/cmd/web/bootstrap"
_ "tm/cmd/web/docs" // This is generated by swag
@@ -143,6 +144,7 @@ func main() {
// Initialize notification service
notificationSDK := bootstrap.InitNotificationService(conf.Notification, logger)
xssPolicy := security.NewXSSPolicy()
// Initialize authorization service
userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger)
@@ -175,7 +177,7 @@ func main() {
tenderService := tender.NewService(tenderRepository, companyService, logger)
feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger)
tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger)
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger)
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger, xssPolicy)
flagService := assets.NewService(logger, conf.Assets.FlagsPath)
notificationService := notification.NewService(notificationSDK, userService, customerService, logger)
contactService := contact.NewService(contactRepository, logger)
@@ -193,7 +195,7 @@ func main() {
tenderHandler := tender.NewHandler(tenderService, logger)
feedbackHandler := feedback.NewHandler(feedbackService, userHandler, customerHandler, logger)
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
inquiryHandler := inquiry.NewHandler(inquiryService, logger)
inquiryHandler := inquiry.NewHandler(inquiryService, logger, xssPolicy)
flagHandler := assets.NewHandler(flagService, logger)
notificationHandler := notification.NewHandler(notificationService)
contactHandler := contact.NewHandler(contactService)
@@ -207,8 +209,8 @@ func main() {
e := bootstrap.InitHTTPServer(conf, logger)
// Register routes
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, scraperHandler)
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler)
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, scraperHandler, xssPolicy)
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, xssPolicy)
// Start server
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
+19 -2
View File
@@ -14,13 +14,23 @@ import (
"tm/internal/tender"
"tm/internal/tender_approval"
"tm/internal/user"
"tm/pkg/security"
"github.com/labstack/echo/v4"
)
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, scraperHandler *scraper.Handler) {
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, scraperHandler *scraper.Handler, xssPolicy *security.XSSPolicy) {
adminV1 := e.Group("/admin/v1")
// Add CSP middleware to admin routes (stricter policy)
adminCSPConfig := security.DefaultCSPConfig()
adminCSPConfig.ScriptSrc = []string{"'self'"} // No unsafe-inline for admin
adminCSPConfig.StyleSrc = []string{"'self'"} // No unsafe-inline for admin
e.Use(security.CSPMiddleware(adminCSPConfig))
// Add CSP report endpoint
e.POST("/api/v1/csp-report", security.CSPReportHandler)
// Admin Users Routes
adminUsersGP := adminV1.Group("/users")
{
@@ -175,9 +185,16 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
}
}
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler) {
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, xssPolicy *security.XSSPolicy) {
v1 := e.Group("/api/v1")
// Add CSP middleware to public routes
cspConfig := security.DefaultCSPConfig()
e.Use(security.CSPMiddleware(cspConfig))
// Add CSP report endpoint
e.POST("/api/v1/csp-report", security.CSPReportHandler)
customerGP := v1.Group("/profile")
{
customerGP.POST("/login", customerHandler.Login)
+3
View File
@@ -7,6 +7,7 @@ toolchain go1.24.4
require (
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/labstack/echo/v4 v4.13.4
github.com/microcosm-cc/bluemonday v1.0.27
github.com/minio/minio-go/v7 v7.0.97
github.com/redis/go-redis/v9 v9.14.0
github.com/robfig/cron/v3 v3.0.1
@@ -21,6 +22,7 @@ require (
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -32,6 +34,7 @@ require (
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
+6
View File
@@ -2,6 +2,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -34,6 +36,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -57,6 +61,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+17 -7
View File
@@ -2,8 +2,11 @@ package inquiry
import (
"fmt"
"html"
"strings"
"time"
"tm/pkg/security"
)
// EmailTemplateData represents the data needed for the email template
@@ -20,7 +23,14 @@ type EmailTemplateData struct {
}
// GenerateNewInquiryEmailTemplate generates a beautiful HTML email template for new inquiry notifications
func GenerateNewInquiryEmailTemplate(data *EmailTemplateData) string {
func GenerateNewInquiryEmailTemplate(data *EmailTemplateData, xssPolicy *security.XSSPolicy) string {
// Sanitize all user inputs before inserting into HTML
safeFullName := html.EscapeString(data.FullName)
safeCompanyName := html.EscapeString(data.CompanyName)
safeWorkEmail := html.EscapeString(data.WorkEmail)
safePhoneNumber := html.EscapeString(data.PhoneNumber)
safeInquiryID := html.EscapeString(data.InquiryID)
// Format the creation date
createdAt := time.Unix(data.CreatedAt, 0).Format("January 2, 2006 at 3:04 PM")
@@ -249,12 +259,12 @@ func GenerateNewInquiryEmailTemplate(data *EmailTemplateData) string {
</body>
</html>`
// Replace placeholders with actual data
html = strings.ReplaceAll(html, "{{COMPANY_NAME}}", data.CompanyName)
html = strings.ReplaceAll(html, "{{INQUIRY_ID}}", data.InquiryID)
html = strings.ReplaceAll(html, "{{FULL_NAME}}", data.FullName)
html = strings.ReplaceAll(html, "{{WORK_EMAIL}}", data.WorkEmail)
html = strings.ReplaceAll(html, "{{PHONE_NUMBER}}", data.PhoneNumber)
// Replace placeholders with sanitized data
html = strings.ReplaceAll(html, "{{COMPANY_NAME}}", safeCompanyName)
html = strings.ReplaceAll(html, "{{INQUIRY_ID}}", safeInquiryID)
html = strings.ReplaceAll(html, "{{FULL_NAME}}", safeFullName)
html = strings.ReplaceAll(html, "{{WORK_EMAIL}}", safeWorkEmail)
html = strings.ReplaceAll(html, "{{PHONE_NUMBER}}", safePhoneNumber)
html = strings.ReplaceAll(html, "{{CREATED_AT}}", createdAt)
return html
+75 -1
View File
@@ -1,6 +1,11 @@
package inquiry
import "tm/pkg/response"
import (
"tm/pkg/response"
"tm/pkg/security"
"github.com/asaskevich/govalidator"
)
// Inquiry DTOs
@@ -83,3 +88,72 @@ func (i *Inquiry) ToResponse() *InquiryResponse {
UpdatedAt: i.UpdatedAt,
}
}
// ValidateAndSanitize validates and sanitizes the form
func (f *CreateInquiryForm) ValidateAndSanitize(xssPolicy *security.XSSPolicy) error {
var err error
// Sanitize and validate FullName
f.FullName, err = xssPolicy.ValidateAndSanitizeInput(f.FullName, "text")
if err != nil {
return err
}
// Sanitize and validate CompanyName
f.CompanyName, err = xssPolicy.ValidateAndSanitizeInput(f.CompanyName, "text")
if err != nil {
return err
}
// Sanitize and validate WorkEmail (email validation is separate)
f.WorkEmail, err = xssPolicy.ValidateAndSanitizeInput(f.WorkEmail, "email")
if err != nil {
return err
}
// Sanitize and validate PhoneNumber
f.PhoneNumber, err = xssPolicy.ValidateAndSanitizeInput(f.PhoneNumber, "phone")
if err != nil {
return err
}
// Standard govalidator validation
_, err = govalidator.ValidateStruct(f)
return err
}
// ValidateAndSanitize validates and sanitizes the status update form
func (f *UpdateInquiryStatusForm) ValidateAndSanitize(xssPolicy *security.XSSPolicy) error {
var err error
// Sanitize Reason
f.Reason, err = xssPolicy.ValidateAndSanitizeInput(f.Reason, "text")
if err != nil {
return err
}
// Sanitize Description
f.Description, err = xssPolicy.ValidateAndSanitizeInput(f.Description, "html")
if err != nil {
return err
}
// Standard govalidator validation
_, err = govalidator.ValidateStruct(f)
return err
}
// ValidateAndSanitize validates and sanitizes the search form
func (f *SearchInquiriesForm) ValidateAndSanitize(xssPolicy *security.XSSPolicy) error {
if f.Search != nil {
sanitized, err := xssPolicy.ValidateAndSanitizeInput(*f.Search, "text")
if err != nil {
return err
}
*f.Search = sanitized
}
// Standard govalidator validation
_, err := govalidator.ValidateStruct(f)
return err
}
+14 -12
View File
@@ -4,22 +4,24 @@ import (
"encoding/json"
"tm/pkg/logger"
"tm/pkg/response"
"tm/pkg/security"
"github.com/asaskevich/govalidator"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for inquiry operations
type Handler struct {
service Service
logger logger.Logger
service Service
logger logger.Logger
xssPolicy *security.XSSPolicy
}
// NewInquiryHandler creates a new inquiry handler
func NewHandler(service Service, logger logger.Logger) *Handler {
func NewHandler(service Service, logger logger.Logger, xssPolicy *security.XSSPolicy) *Handler {
return &Handler{
service: service,
logger: logger,
service: service,
logger: logger,
xssPolicy: xssPolicy,
}
}
@@ -42,8 +44,8 @@ func (h *Handler) CreateInquiry(c echo.Context) error {
return response.BadRequest(c, "Invalid request format", "")
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
// Validate and sanitize form
if err := form.ValidateAndSanitize(h.xssPolicy); err != nil {
return response.ValidationError(c, err.Error(), "")
}
@@ -122,8 +124,8 @@ func (h *Handler) UpdateInquiryStatus(c echo.Context) error {
return response.BadRequest(c, "Invalid request format", "")
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
// Validate and sanitize form
if err := form.ValidateAndSanitize(h.xssPolicy); err != nil {
return response.ValidationError(c, err.Error(), "")
}
@@ -163,8 +165,8 @@ func (h *Handler) SearchInquiries(c echo.Context) error {
return response.BadRequest(c, "Invalid request format", "")
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
// Validate and sanitize form
if err := form.ValidateAndSanitize(h.xssPolicy); err != nil {
return response.ValidationError(c, err.Error(), "")
}
+6 -3
View File
@@ -7,6 +7,7 @@ import (
"tm/pkg/logger"
"tm/pkg/notification"
"tm/pkg/response"
"tm/pkg/security"
)
// Service defines business logic for inquiry operations
@@ -23,14 +24,16 @@ type inquiryService struct {
repository Repository
sdk notification.SDK
logger logger.Logger
xssPolicy *security.XSSPolicy
}
// NewService creates a new inquiry service
func NewService(repository Repository, sdk notification.SDK, logger logger.Logger) Service {
func NewService(repository Repository, sdk notification.SDK, logger logger.Logger, xssPolicy *security.XSSPolicy) Service {
return &inquiryService{
repository: repository,
sdk: sdk,
logger: logger,
xssPolicy: xssPolicy,
}
}
@@ -114,7 +117,7 @@ func (s *inquiryService) Create(ctx context.Context, form *CreateInquiryForm) (*
CompanyLogo: "",
CompanyURL: "https://opplens.com",
SupportEmail: "info@opplens.com",
}),
}, s.xssPolicy),
Type: "alert",
Priority: "important",
EventType: notification.EventTypeEmail,
@@ -137,7 +140,7 @@ func (s *inquiryService) Create(ctx context.Context, form *CreateInquiryForm) (*
CompanyLogo: "",
CompanyURL: "https://opplens.com",
SupportEmail: "info@opplens.com",
}),
}, s.xssPolicy),
Type: "alert",
Priority: "important",
EventType: notification.EventTypeEmail,
+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
}
}