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.
This commit is contained in:
+13
-1
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user