From 11a050c86d88547d0a311729dc9bbb1f67948895 Mon Sep 17 00:00:00 2001 From: DevipriyaS17 Date: Wed, 12 Aug 2026 14:41:58 +0530 Subject: [PATCH] fix: add clickjacking protection headers --- internal/controller/httpapi/router.go | 14 ++++++ internal/controller/httpapi/router_test.go | 53 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 internal/controller/httpapi/router_test.go diff --git a/internal/controller/httpapi/router.go b/internal/controller/httpapi/router.go index b1dd2b670..04899031e 100644 --- a/internal/controller/httpapi/router.go +++ b/internal/controller/httpapi/router.go @@ -19,11 +19,25 @@ import ( "github.com/device-management-toolkit/console/pkg/logger" ) +const ( + xFrameOptionsHeaderValue = "SAMEORIGIN" + contentSecurityPolicyHeaderValue = "frame-ancestors 'self'" +) + +func clickjackingProtectionMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("X-Frame-Options", xFrameOptionsHeaderValue) + c.Header("Content-Security-Policy", contentSecurityPolicyHeaderValue) + c.Next() + } +} + // NewRouter -. func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg *config.Config) { // Options handler.Use(gin.Logger()) handler.Use(gin.Recovery()) + handler.Use(clickjackingProtectionMiddleware()) // Add Prometheus middleware for automatic HTTP metrics // Don't automatically register /metrics endpoint - we have our own diff --git a/internal/controller/httpapi/router_test.go b/internal/controller/httpapi/router_test.go new file mode 100644 index 000000000..7f8460879 --- /dev/null +++ b/internal/controller/httpapi/router_test.go @@ -0,0 +1,53 @@ +package httpapi + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + + "github.com/device-management-toolkit/console/config" + "github.com/device-management-toolkit/console/internal/usecase" + "github.com/device-management-toolkit/console/pkg/logger" +) + +func TestClickjackingProtectionMiddlewareSetsHeaders(t *testing.T) { + t.Parallel() + + engine := gin.New() + engine.Use(clickjackingProtectionMiddleware()) + engine.GET("/healthz", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodGet, "/healthz", http.NoBody) + w := httptest.NewRecorder() + + engine.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, xFrameOptionsHeaderValue, w.Header().Get("X-Frame-Options")) + require.Equal(t, contentSecurityPolicyHeaderValue, w.Header().Get("Content-Security-Policy")) +} + +//nolint:paralleltest // mutates shared global config.ConsoleConfig +func TestNewRouterAuthorizeHasClickjackingHeaders(t *testing.T) { + prev := config.ConsoleConfig + config.ConsoleConfig = &config.Config{} + + t.Cleanup(func() { config.ConsoleConfig = prev }) + + engine := gin.New() + NewRouter(engine, logger.New("error"), usecase.Usecases{}, &config.Config{}) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/authorize", http.NoBody) + w := httptest.NewRecorder() + + engine.ServeHTTP(w, req) + + require.Equal(t, http.StatusBadRequest, w.Code) + require.Equal(t, xFrameOptionsHeaderValue, w.Header().Get("X-Frame-Options")) + require.Equal(t, contentSecurityPolicyHeaderValue, w.Header().Get("Content-Security-Policy")) +}