fix(artwork): enforce entity visibility on the Subsonic getCoverArt path

serveEntity reads persisted item_artwork by id, bypassing the library and private-
playlist filters that the legacy entity-load applied. On the authenticated Subsonic
path a user could fetch artwork for an inaccessible album or someone else's private
playlist by guessing an id. getCoverArt now resolves the underlying entity through
the request-scoped (filtered) repositories and serves the placeholder when it is not
visible, so existence isn't leaked and the always-an-image invariant holds. The
public share (JWT-authorized) and Jellyfin (admin) paths are intentionally untouched.
This commit is contained in:
Deluan 2026-07-23 08:52:31 -04:00
commit 9dd306eb10
3 changed files with 66 additions and 0 deletions

View file

@ -308,6 +308,10 @@ func (s *service) enqueue(ctx context.Context, artID model.ArtworkID, priority i
}
func (s *service) placeholder(kind model.Kind) *Image {
return placeholderImage(kind)
}
func placeholderImage(kind model.Kind) *Image {
path := consts.PlaceholderAlbumArt
if kind == model.KindArtistArtwork {
path = consts.PlaceholderArtistArt
@ -316,6 +320,13 @@ func (s *service) placeholder(kind model.Kind) *Image {
return &Image{ReadCloser: r, Placeholder: true}
}
// PlaceholderFor returns the kind-appropriate placeholder for an artwork id, for callers that must
// serve a placeholder without consulting persisted state (e.g. an access-control denial).
func PlaceholderFor(id string) *Image {
artID, _ := model.ParseArtworkID(id)
return placeholderImage(artID.Kind)
}
type coverArtIDGetter interface {
CoverArtID() model.ArtworkID
}