Compare commits

...

1 Commits

Author SHA1 Message Date
Deluan
0a4722802a 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.
2026-02-08 10:33:46 -05:00
2 changed files with 56 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"regexp"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@@ -26,6 +27,8 @@ import (
const Version = "1.16.1" 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 handler = func(*http.Request) (*responses.Subsonic, error)
type handlerRaw = func(http.ResponseWriter, *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} wrapper := &responses.JsonWrapper{Subsonic: *payload}
response, err = json.Marshal(wrapper) response, err = json.Marshal(wrapper)
case "jsonp": case "jsonp":
w.Header().Set("Content-Type", "application/javascript")
callback, _ := p.String("callback") 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} wrapper := &responses.JsonWrapper{Subsonic: *payload}
response, err = json.Marshal(wrapper) response, err = json.Marshal(wrapper)
response = fmt.Appendf(nil, "%s(%s)", callback, response) response = fmt.Appendf(nil, "%s(%s)", callback, response)

View File

@@ -73,6 +73,49 @@ var _ = Describe("sendResponse", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(wrapper.Subsonic.Status).To(Equal(payload.Status)) 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() { When("format is XML or unspecified", func() {