From 0a4722802af833caa8dd794fa8c25f8d0a97dd06 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 8 Feb 2026 10:33:46 -0500 Subject: [PATCH] fix(subsonic): validate JSONP callback parameter Added validation to ensure the JSONP callback parameter is a valid JavaScript identifier before reflecting it into the response. Invalid callbacks now return a JSON error response instead. This prevents malicious input from being injected into the response body via the callback parameter. --- server/subsonic/api.go | 14 +++++++++++- server/subsonic/api_test.go | 43 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/server/subsonic/api.go b/server/subsonic/api.go index aa7d0be9..1d13e2c0 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -26,6 +27,8 @@ import ( const Version = "1.16.1" +var validJSIdentifier = regexp.MustCompile(`^[a-zA-Z_$][a-zA-Z0-9_$.]*$`) + type handler = func(*http.Request) (*responses.Subsonic, error) type handlerRaw = func(http.ResponseWriter, *http.Request) (*responses.Subsonic, error) @@ -315,8 +318,17 @@ func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub wrapper := &responses.JsonWrapper{Subsonic: *payload} response, err = json.Marshal(wrapper) case "jsonp": - w.Header().Set("Content-Type", "application/javascript") callback, _ := p.String("callback") + if !validJSIdentifier.MatchString(callback) { + log.Warn(r.Context(), "Invalid JSONP callback parameter", "callback", callback) + w.Header().Set("Content-Type", "application/json") + errResp := newResponse() + errResp.Status = responses.StatusFailed + errResp.Error = &responses.Error{Code: responses.ErrorGeneric, Message: "invalid callback parameter"} + response, _ = json.Marshal(responses.JsonWrapper{Subsonic: *errResp}) + break + } + w.Header().Set("Content-Type", "application/javascript") wrapper := &responses.JsonWrapper{Subsonic: *payload} response, err = json.Marshal(wrapper) response = fmt.Appendf(nil, "%s(%s)", callback, response) diff --git a/server/subsonic/api_test.go b/server/subsonic/api_test.go index eaecd7c0..3b88704b 100644 --- a/server/subsonic/api_test.go +++ b/server/subsonic/api_test.go @@ -73,6 +73,49 @@ var _ = Describe("sendResponse", func() { Expect(err).NotTo(HaveOccurred()) Expect(wrapper.Subsonic.Status).To(Equal(payload.Status)) }) + + It("should accept valid callback names with dots", func() { + q := r.URL.Query() + q.Add("f", "jsonp") + q.Add("callback", "jQuery.callback_123") + r.URL.RawQuery = q.Encode() + + sendResponse(w, r, payload) + + body := w.Body.String() + Expect(body).To(HavePrefix("jQuery.callback_123(")) + }) + + It("should reject callback with invalid characters", func() { + q := r.URL.Query() + q.Add("f", "jsonp") + q.Add("callback", "alert(1)//") + r.URL.RawQuery = q.Encode() + + sendResponse(w, r, payload) + + Expect(w.Header().Get("Content-Type")).To(Equal("application/json")) + var wrapper responses.JsonWrapper + err := json.Unmarshal(w.Body.Bytes(), &wrapper) + Expect(err).NotTo(HaveOccurred()) + Expect(wrapper.Subsonic.Status).To(Equal(responses.StatusFailed)) + Expect(wrapper.Subsonic.Error.Message).To(ContainSubstring("invalid callback parameter")) + }) + + It("should reject empty callback parameter", func() { + q := r.URL.Query() + q.Add("f", "jsonp") + q.Add("callback", "") + r.URL.RawQuery = q.Encode() + + sendResponse(w, r, payload) + + Expect(w.Header().Get("Content-Type")).To(Equal("application/json")) + var wrapper responses.JsonWrapper + err := json.Unmarshal(w.Body.Bytes(), &wrapper) + Expect(err).NotTo(HaveOccurred()) + Expect(wrapper.Subsonic.Status).To(Equal(responses.StatusFailed)) + }) }) When("format is XML or unspecified", func() {