From 68cbd05e520aaeadb4a9fa7b68abb37e8f3d5043 Mon Sep 17 00:00:00 2001 From: Rohan Sood <56945243+rohansood10@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:58:32 -0700 Subject: [PATCH] fix: Add X-Frame-Options and CSP headers to Swagger UI endpoints (#26521) Signed-off-by: rohansood10 Signed-off-by: Blake Pettersson Co-authored-by: rohansood10 Co-authored-by: Blake Pettersson Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- util/swagger/swagger.go | 17 +++++++++++++---- util/swagger/swagger_test.go | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/util/swagger/swagger.go b/util/swagger/swagger.go index 0509939129..de36da9e5c 100644 --- a/util/swagger/swagger.go +++ b/util/swagger/swagger.go @@ -11,20 +11,29 @@ import ( // filename of ReDoc script in UI's assets/scripts path const redocScriptName = "redoc.standalone.js" +// withFrameOptions wraps an http.Handler to set headers that prevent iframe embedding (clickjacking protection). +func withFrameOptions(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Content-Security-Policy", "frame-ancestors 'none'") + h.ServeHTTP(w, r) + }) +} + // ServeSwaggerUI serves the Swagger UI and JSON spec. func ServeSwaggerUI(mux *http.ServeMux, swaggerJSON string, uiPath string, rootPath string) { prefix := path.Dir(uiPath) swaggerPath := path.Join(prefix, "swagger.json") - mux.HandleFunc(swaggerPath, func(w http.ResponseWriter, _ *http.Request) { + mux.Handle(swaggerPath, withFrameOptions(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, swaggerJSON) - }) + }))) specURL := path.Join(prefix, rootPath, "swagger.json") scriptURL := path.Join(prefix, rootPath, "assets", "scripts", redocScriptName) - mux.Handle(uiPath, middleware.Redoc(middleware.RedocOpts{ + mux.Handle(uiPath, withFrameOptions(middleware.Redoc(middleware.RedocOpts{ BasePath: prefix, SpecURL: specURL, Path: path.Base(uiPath), RedocURL: scriptURL, - }, http.NotFoundHandler())) + }, http.NotFoundHandler()))) } diff --git a/util/swagger/swagger_test.go b/util/swagger/swagger_test.go index dbf938e9e0..47ab8e6905 100644 --- a/util/swagger/swagger_test.go +++ b/util/swagger/swagger_test.go @@ -52,4 +52,19 @@ func TestSwaggerUI(t *testing.T) { require.NoError(t, err) require.Equalf(t, http.StatusOK, resp.StatusCode, "Was expecting status code 200 from swagger-ui, but got %d instead", resp.StatusCode) require.NoError(t, resp.Body.Close()) + + // Verify clickjacking protection headers on swagger.json + require.Equal(t, "DENY", resp.Header.Get("X-Frame-Options")) + require.Equal(t, "frame-ancestors 'none'", resp.Header.Get("Content-Security-Policy")) + + // Verify clickjacking protection headers on swagger-ui + uiReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, server+"/swagger-ui", http.NoBody) + require.NoError(t, err) + + uiResp, err := http.DefaultClient.Do(uiReq) + require.NoError(t, err) + require.Equalf(t, http.StatusOK, uiResp.StatusCode, "Was expecting status code 200 from swagger-ui, but got %d instead", uiResp.StatusCode) + require.Equal(t, "DENY", uiResp.Header.Get("X-Frame-Options")) + require.Equal(t, "frame-ancestors 'none'", uiResp.Header.Get("Content-Security-Policy")) + require.NoError(t, uiResp.Body.Close()) }