mirror of
https://github.com/navidrome/navidrome.git
synced 2026-02-09 14:31:11 -05:00
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.
171 lines
5.1 KiB
Go
171 lines
5.1 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"math"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
|
"github.com/navidrome/navidrome/utils/gg"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
var _ = Describe("sendResponse", func() {
|
|
var (
|
|
w *httptest.ResponseRecorder
|
|
r *http.Request
|
|
payload *responses.Subsonic
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
w = httptest.NewRecorder()
|
|
r = httptest.NewRequest("GET", "/somepath", nil)
|
|
payload = &responses.Subsonic{
|
|
Status: responses.StatusOK,
|
|
Version: "1.16.1",
|
|
}
|
|
})
|
|
|
|
When("format is JSON", func() {
|
|
It("should set Content-Type to application/json and return the correct body", func() {
|
|
q := r.URL.Query()
|
|
q.Add("f", "json")
|
|
r.URL.RawQuery = q.Encode()
|
|
|
|
sendResponse(w, r, payload)
|
|
|
|
Expect(w.Header().Get("Content-Type")).To(Equal("application/json"))
|
|
Expect(w.Body.String()).NotTo(BeEmpty())
|
|
|
|
var wrapper responses.JsonWrapper
|
|
err := json.Unmarshal(w.Body.Bytes(), &wrapper)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(wrapper.Subsonic.Status).To(Equal(payload.Status))
|
|
Expect(wrapper.Subsonic.Version).To(Equal(payload.Version))
|
|
})
|
|
})
|
|
|
|
When("format is JSONP", func() {
|
|
It("should set Content-Type to application/javascript and return the correct callback body", func() {
|
|
q := r.URL.Query()
|
|
q.Add("f", "jsonp")
|
|
q.Add("callback", "testCallback")
|
|
r.URL.RawQuery = q.Encode()
|
|
|
|
sendResponse(w, r, payload)
|
|
|
|
Expect(w.Header().Get("Content-Type")).To(Equal("application/javascript"))
|
|
body := w.Body.String()
|
|
Expect(body).To(SatisfyAll(
|
|
HavePrefix("testCallback("),
|
|
HaveSuffix(")"),
|
|
))
|
|
|
|
// Extract JSON from the JSONP response
|
|
jsonBody := body[strings.Index(body, "(")+1 : strings.LastIndex(body, ")")]
|
|
var wrapper responses.JsonWrapper
|
|
err := json.Unmarshal([]byte(jsonBody), &wrapper)
|
|
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() {
|
|
It("should set Content-Type to application/xml and return the correct body", func() {
|
|
// No format specified, expecting XML by default
|
|
sendResponse(w, r, payload)
|
|
|
|
Expect(w.Header().Get("Content-Type")).To(Equal("application/xml"))
|
|
var subsonicResponse responses.Subsonic
|
|
err := xml.Unmarshal(w.Body.Bytes(), &subsonicResponse)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(subsonicResponse.Status).To(Equal(payload.Status))
|
|
Expect(subsonicResponse.Version).To(Equal(payload.Version))
|
|
})
|
|
})
|
|
|
|
When("an error occurs during marshalling", func() {
|
|
It("should return a fail response", func() {
|
|
payload.Song = &responses.Child{OpenSubsonicChild: &responses.OpenSubsonicChild{}}
|
|
// An +Inf value will cause an error when marshalling to JSON
|
|
payload.Song.ReplayGain = responses.ReplayGain{TrackGain: gg.P(math.Inf(1))}
|
|
q := r.URL.Query()
|
|
q.Add("f", "json")
|
|
r.URL.RawQuery = q.Encode()
|
|
|
|
sendResponse(w, r, payload)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
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.Version).To(Equal(payload.Version))
|
|
Expect(wrapper.Subsonic.Error.Message).To(ContainSubstring("json: unsupported value: +Inf"))
|
|
})
|
|
})
|
|
|
|
It("updates status pointer when an error occurs", func() {
|
|
pointer := int32(0)
|
|
|
|
ctx := context.WithValue(r.Context(), subsonicErrorPointer, &pointer)
|
|
r = r.WithContext(ctx)
|
|
|
|
payload.Status = responses.StatusFailed
|
|
payload.Error = &responses.Error{Code: responses.ErrorDataNotFound}
|
|
|
|
sendResponse(w, r, payload)
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
Expect(pointer).To(Equal(responses.ErrorDataNotFound))
|
|
})
|
|
})
|