diff --git a/cmd/web/main.go b/cmd/web/main.go index 98216da..f9c238e 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -248,6 +248,8 @@ func main() { // Initialize HTTP server e := bootstrap.InitHTTPServer(conf, logger) + router.SetupCSPSecurity(e) + // Register routes router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, kanbanHandler, fileStoreHandler, dashboardHandler, xssPolicy) router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, fileStoreHandler, xssPolicy) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 9b15f05..c1e73fc 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -22,17 +22,20 @@ import ( "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, kanbanHandler *kanban.Handler, fileStoreHandler *filestore.Handler, dashboardHandler *dashboard.Handler, xssPolicy *security.XSSPolicy) { - adminV1 := e.Group("/admin/v1") - - // Add CSP middleware to admin routes (stricter policy) +// SetupCSPSecurity registers global CSP middleware and the CSP report endpoint once. +func SetupCSPSecurity(e *echo.Echo) { 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 + publicCSPConfig := security.DefaultCSPConfig() + + e.Use(security.CSPMiddlewareForPaths("/admin", adminCSPConfig, "/api/v1", publicCSPConfig)) e.POST("/api/v1/csp-report", security.CSPReportHandler) +} + +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, kanbanHandler *kanban.Handler, fileStoreHandler *filestore.Handler, dashboardHandler *dashboard.Handler, xssPolicy *security.XSSPolicy) { + adminV1 := e.Group("/admin/v1") // Admin Users Routes adminUsersGP := adminV1.Group("/users") @@ -221,13 +224,6 @@ 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, fileStoreHandler *filestore.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) diff --git a/internal/document_scraper/middleware.go b/internal/document_scraper/middleware.go index 9fc4f2b..360676a 100644 --- a/internal/document_scraper/middleware.go +++ b/internal/document_scraper/middleware.go @@ -1,6 +1,7 @@ package document_scraper import ( + "crypto/subtle" "strings" "tm/pkg/response" @@ -25,7 +26,7 @@ func APIKeyMiddleware(apiKey string) echo.MiddlewareFunc { return response.Unauthorized(c, "API key required. Use X-API-Key header or Authorization: ApiKey ") } - if apiKeyHeader != apiKey { + if subtle.ConstantTimeCompare([]byte(apiKeyHeader), []byte(apiKey)) != 1 { return response.Unauthorized(c, "Invalid API key") } diff --git a/pkg/security/csp.go b/pkg/security/csp.go index 7bf066c..e32fbdc 100644 --- a/pkg/security/csp.go +++ b/pkg/security/csp.go @@ -108,6 +108,27 @@ func CSPMiddleware(config *CSPConfig) echo.MiddlewareFunc { } } +// 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 {