From 550ee9f7b9efa1ff108ad4cb007afaa34daf1238 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Sat, 14 Mar 2026 14:27:41 +0200 Subject: [PATCH] Fix stored XSS in /store endpoint (CVE-2025-70849) Set Content-Type to application/octet-stream in storeReadHandler to prevent Go's content sniffing from serving HTML payloads as text/html. Add X-Content-Type-Options: nosniff to prevent browsers from overriding Content-Type via MIME sniffing, and Content-Security-Policy: default-src 'none' to block script execution as defense-in-depth. Signed-off-by: Stefan Prodan --- pkg/api/http/store.go | 3 +++ pkg/api/http/store_test.go | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 pkg/api/http/store_test.go diff --git a/pkg/api/http/store.go b/pkg/api/http/store.go index 86efae7..1a85f54 100644 --- a/pkg/api/http/store.go +++ b/pkg/api/http/store.go @@ -60,6 +60,9 @@ func (s *Server) storeReadHandler(w http.ResponseWriter, r *http.Request) { s.ErrorResponse(w, r, span, "reading file failed", http.StatusInternalServerError) return } + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Content-Security-Policy", "default-src 'none'") w.WriteHeader(http.StatusAccepted) w.Write([]byte(content)) } diff --git a/pkg/api/http/store_test.go b/pkg/api/http/store_test.go new file mode 100644 index 0000000..ab52289 --- /dev/null +++ b/pkg/api/http/store_test.go @@ -0,0 +1,54 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gorilla/mux" +) + +func TestStoreReadHandler_ContentType(t *testing.T) { + dataDir := t.TempDir() + srv := NewMockServer() + srv.config.DataPath = dataDir + + // Write an HTML payload to the store. + writeReq, err := http.NewRequest("POST", "/store", strings.NewReader("")) + if err != nil { + t.Fatal(err) + } + writeRR := httptest.NewRecorder() + http.HandlerFunc(srv.storeWriteHandler).ServeHTTP(writeRR, writeReq) + + if writeRR.Code != http.StatusAccepted { + t.Fatalf("store write returned status %d, want %d", writeRR.Code, http.StatusAccepted) + } + + // Read it back and verify Content-Type is application/octet-stream, not text/html. + hash := hash("") + readReq, err := http.NewRequest("GET", "/store/"+hash, nil) + if err != nil { + t.Fatal(err) + } + readReq = mux.SetURLVars(readReq, map[string]string{"hash": hash}) + + readRR := httptest.NewRecorder() + http.HandlerFunc(srv.storeReadHandler).ServeHTTP(readRR, readReq) + + if readRR.Code != http.StatusAccepted { + t.Fatalf("store read returned status %d, want %d", readRR.Code, http.StatusAccepted) + } + + expectedHeaders := map[string]string{ + "Content-Type": "application/octet-stream", + "X-Content-Type-Options": "nosniff", + "Content-Security-Policy": "default-src 'none'", + } + for header, want := range expectedHeaders { + if got := readRR.Header().Get(header); got != want { + t.Errorf("%s = %q, want %q", header, got, want) + } + } +}