Skip to content

Commit 6dd71e6

Browse files
committed
fix(subsonic): enforce playlist ownership on getPlaylist/deletePlaylist
1 parent 37090aa commit 6dd71e6

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

‎server/ctrlsubsonic/handlers_playlist.go‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func (c *Controller) ServeGetPlaylists(r *http.Request) *spec.Response {
4949
}
5050

5151
func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
52+
user := r.Context().Value(CtxUser).(*db.User)
5253
params := r.Context().Value(CtxParams).(params.Params)
5354
playlistID, err := params.GetFirstID("id", "playlistId")
5455
if err != nil {
@@ -58,6 +59,9 @@ func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
5859
if err != nil {
5960
return spec.NewError(70, "playlist with id %s not found", playlistID)
6061
}
62+
if playlist.UserID != user.ID && !playlist.IsPublic {
63+
return spec.NewError(50, "you aren't allowed to read that user's playlist")
64+
}
6165
sub := spec.NewResponse()
6266
rendered, err := playlistRender(c, params, playlist, playlistID, true)
6367
if err != nil {
@@ -175,12 +179,21 @@ func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
175179
}
176180

177181
func (c *Controller) ServeDeletePlaylist(r *http.Request) *spec.Response {
182+
user := r.Context().Value(CtxUser).(*db.User)
178183
params := r.Context().Value(CtxParams).(params.Params)
179184
playlistID, err := params.GetFirstID("id", "playlistId")
180185
if err != nil {
181186
return spec.NewError(10, "please provide an `id` or `playlistId` parameter")
182187
}
183-
if err := c.playlistStore.Delete(playlistIDDecode(playlistID)); err != nil {
188+
playlistPath := playlistIDDecode(playlistID)
189+
playlist, err := c.playlistStore.Read(playlistPath)
190+
if err != nil {
191+
return spec.NewError(70, "playlist with id %s not found", playlistID)
192+
}
193+
if playlist.UserID != 0 && playlist.UserID != user.ID {
194+
return spec.NewError(50, "you aren't allowed to delete that user's playlist")
195+
}
196+
if err := c.playlistStore.Delete(playlistPath); err != nil {
184197
return spec.NewError(0, "delete playlist: %v", err)
185198
}
186199
return spec.NewResponse()

‎server/ctrlsubsonic/handlers_playlist_test.go‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99
"time"
1010

11+
playlistp "go.senan.xyz/gonic/playlist"
1112
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
1213
)
1314

@@ -115,3 +116,55 @@ func TestDeletePlaylist(t *testing.T) {
115116
query{url.Values{}, "after_delete", false},
116117
)
117118
}
119+
120+
func TestGetPlaylistDeniesOtherUsersPrivate(t *testing.T) {
121+
t.Parallel()
122+
f := newFixture(t)
123+
124+
privateID := writePrivatePlaylist(t, f)
125+
126+
body := f.query(t, f.contr.ServeGetPlaylist, f.alt, url.Values{
127+
"id": {privateID},
128+
})
129+
var sub spec.SubsonicResponse
130+
if err := json.Unmarshal([]byte(body), &sub); err != nil {
131+
t.Fatalf("unmarshal: %v", err)
132+
}
133+
if sub.Response.Status != "failed" || sub.Response.Error == nil || sub.Response.Error.Code != 50 {
134+
t.Fatalf("expected error 50, got: %s", body)
135+
}
136+
}
137+
138+
func TestDeletePlaylistDeniesOtherUsers(t *testing.T) {
139+
t.Parallel()
140+
f := newFixture(t)
141+
142+
body := f.query(t, f.contr.ServeDeletePlaylist, f.alt, url.Values{
143+
"id": {f.sharedPlaylistID()},
144+
})
145+
var sub spec.SubsonicResponse
146+
if err := json.Unmarshal([]byte(body), &sub); err != nil {
147+
t.Fatalf("unmarshal: %v", err)
148+
}
149+
if sub.Response.Status != "failed" || sub.Response.Error == nil || sub.Response.Error.Code != 50 {
150+
t.Fatalf("expected error 50, got: %s", body)
151+
}
152+
if _, err := f.contr.playlistStore.Read(filepath.Join("1", "shared.m3u")); err != nil {
153+
t.Fatalf("playlist was deleted despite auth failure: %v", err)
154+
}
155+
}
156+
157+
func writePrivatePlaylist(t *testing.T, f *fixture) string {
158+
t.Helper()
159+
relPath := filepath.Join("1", "private.m3u")
160+
err := f.contr.playlistStore.Write(relPath, &playlistp.Playlist{
161+
UserID: f.admin.ID,
162+
UpdatedAt: time.Date(2020, 5, 1, 12, 0, 0, 0, time.UTC),
163+
Name: "private playlist",
164+
IsPublic: false,
165+
})
166+
if err != nil {
167+
t.Fatalf("write private playlist: %v", err)
168+
}
169+
return playlistIDEncode(relPath).String()
170+
}

0 commit comments

Comments
 (0)