From 426232e6997a0f3d741a311f4a6d8ebd6034f78d Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 10 May 2026 13:32:47 +0200 Subject: [PATCH] fix(security): close go/reflected-xss alerts via html.EscapeString MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL flagged five reflected-XSS sites where caller-supplied query parameters or path segments were concatenated into HTML responses without escaping: * handlers_mgmt.go:147 — Spotify oauth error landing page * handlers_mgmt.go:490 — Amazon oauth error landing page * handlers_docs.go:65 — built from r.URL.Path * recorder_middleware.go:83, mirror_middleware.go:205 — passthrough Write()s carrying tainted bytes from the three sources above Wrap each user-controlled value in html.EscapeString before it lands in the HTML body. The escaped output covers the upstream sources so the middleware passthrough alerts close as well. For handlers_docs the rendered markdown (`output`) and sidebar are server-controlled (loaded from on-disk doc files) and intentionally contain HTML, so only the URL path is escaped — the documentation content itself still renders normally. Handler test suite passes; pre-existing TestDocsConsistency failure about untracked working-tree docs is unrelated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- pkg/service/handlers/handlers_docs.go | 10 ++++++++-- pkg/service/handlers/handlers_mgmt.go | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/service/handlers/handlers_docs.go b/pkg/service/handlers/handlers_docs.go index 145d95c..695ba68 100644 --- a/pkg/service/handlers/handlers_docs.go +++ b/pkg/service/handlers/handlers_docs.go @@ -2,6 +2,7 @@ package handlers import ( "fmt" + "html" "net/http" "os" "path/filepath" @@ -56,7 +57,12 @@ func (s *Server) HandleDocs(w http.ResponseWriter, r *http.Request) { // Render markdown to HTML output := blackfriday.Run(content) - // Wrap in a documentation template with sidebar + // Wrap in a documentation template with sidebar. The user-supplied path + // is escaped before interpolation; the sidebar and rendered markdown + // output are server-controlled (loaded from local files) and may + // legitimately contain HTML. + titleSafe := html.EscapeString(path) + w.Header().Set("Content-Type", "text/html") _, _ = fmt.Fprintf(w, `<!DOCTYPE html> <html> @@ -95,7 +101,7 @@ func (s *Server) HandleDocs(w http.ResponseWriter, r *http.Request) { </div> </div> </body> -</html>`, path, sidebar, output) +</html>`, titleSafe, sidebar, output) } // fixSidebarLinks ensures that relative links in the SUMMARY.md (sidebar) diff --git a/pkg/service/handlers/handlers_mgmt.go b/pkg/service/handlers/handlers_mgmt.go index 422dbfc..d1cbccf 100644 --- a/pkg/service/handlers/handlers_mgmt.go +++ b/pkg/service/handlers/handlers_mgmt.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "html" "io" "log" "net/http" @@ -144,7 +145,9 @@ func (s *Server) HandleMgmtSpotifyCallback(w http.ResponseWriter, r *http.Reques if errMsg := r.URL.Query().Get("error"); errMsg != "" { w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(`<html><body><h1>Spotify Authorization Failed</h1><p>Error: ` + errMsg + `</p></body></html>`)) + // html.EscapeString neutralises any HTML metacharacters in the + // caller-supplied error string before it lands in the response. + _, _ = w.Write([]byte(`<html><body><h1>Spotify Authorization Failed</h1><p>Error: ` + html.EscapeString(errMsg) + `</p></body></html>`)) return } @@ -487,7 +490,9 @@ func (s *Server) HandleMgmtAmazonCallback(w http.ResponseWriter, r *http.Request if errMsg := r.URL.Query().Get("error"); errMsg != "" { w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(`<html><body><h1>Amazon Authorization Failed</h1><p>Error: ` + errMsg + `</p></body></html>`)) + // html.EscapeString neutralises any HTML metacharacters in the + // caller-supplied error string before it lands in the response. + _, _ = w.Write([]byte(`<html><body><h1>Amazon Authorization Failed</h1><p>Error: ` + html.EscapeString(errMsg) + `</p></body></html>`)) return }