navidrome/persistence/player_repository.go
Deluan Quintão 9e8811e4c5
fix(server): enforce player ownership on create and registration (#6184)
POST /api/player passed the ownership check using the userId from the request body, then
saved with the body id. When that id belonged to another user's player, the save became an
update with no owner restriction, overwriting the row and moving it to the caller. Save now
always creates a new player and ignores any id in the body; edits keep going through the
owner-scoped Update.

Player registration also reused a player by the id sent in the Subsonic player cookie or the
Jellyfin DeviceId without checking its owner, letting a user attach to another user's player
and overwrite its name, user agent and IP. Register now only reuses a player owned by the
requesting user, falling back to the user's own players otherwise.
2026-09-20 21:10:41 -04:00

145 lines
4.0 KiB
Go

package persistence
import (
"context"
. "github.com/Masterminds/squirrel"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/model"
"github.com/pocketbase/dbx"
)
type playerRepository struct {
sqlRepository
}
func NewPlayerRepository(ctx context.Context, db dbx.Builder) model.PlayerRepository {
r := &playerRepository{}
r.ctx = ctx
r.db = db
r.registerModel(&model.Player{}, map[string]filterFunc{
"name": containsFilter("player.name"),
})
r.setSortMappings(map[string]string{
"user_name": "username", //TODO rename all user_name and userName to username
})
return r
}
func (r *playerRepository) Put(p *model.Player) error {
_, err := r.put(p.ID, p)
return err
}
func (r *playerRepository) selectPlayer(options ...model.QueryOptions) SelectBuilder {
return r.newSelect(options...).
Columns("player.*").
Join("user ON player.user_id = user.id").
Columns("user.user_name username")
}
func (r *playerRepository) Get(id string) (*model.Player, error) {
sel := r.selectPlayer().Where(Eq{"player.id": id})
var res model.Player
err := r.queryOne(sel, &res)
return &res, err
}
func (r *playerRepository) FindMatch(userId, client, userAgent string) (*model.Player, error) {
sel := r.selectPlayer().Where(And{
Eq{"client": client},
Eq{"user_agent": userAgent},
Eq{"user_id": userId},
})
var res model.Player
err := r.queryOne(sel, &res)
return &res, err
}
func (r *playerRepository) newRestSelect(options ...model.QueryOptions) SelectBuilder {
s := r.selectPlayer(options...)
return s.Where(r.addRestriction())
}
func (r *playerRepository) CountByClient(options ...model.QueryOptions) (map[string]int64, error) {
sel := r.newSelect(options...).
Columns(
"case when client = 'NavidromeUI' then name else client end as player",
"count(*) as count",
).GroupBy("client")
var res []struct {
Player string
Count int64
}
err := r.queryAll(sel, &res)
if err != nil {
return nil, err
}
counts := make(map[string]int64, len(res))
for _, c := range res {
counts[c.Player] = c.Count
}
return counts, nil
}
func (r *playerRepository) CountAll(options ...model.QueryOptions) (int64, error) {
return r.count(r.newRestSelect(), options...)
}
func (r *playerRepository) Count(options ...rest.QueryOptions) (int64, error) {
return r.CountAll(r.parseRestOptions(r.ctx, options...))
}
func (r *playerRepository) Read(id string) (any, error) {
sel := r.newRestSelect().Where(Eq{"player.id": id})
var res model.Player
err := r.queryOne(sel, &res)
return &res, err
}
func (r *playerRepository) ReadAll(options ...rest.QueryOptions) (any, error) {
sel := r.newRestSelect(r.parseRestOptions(r.ctx, options...))
res := model.Players{}
err := r.queryAll(sel, &res)
return res, err
}
func (r *playerRepository) EntityName() string {
return "player"
}
func (r *playerRepository) NewInstance() any {
return &model.Player{}
}
// isPermitted authorizes creating a new record, based on the owner declared in the request body.
// This is only safe for inserts: there is no stored row yet, and a non-admin may only create a
// player they own. Updates must not use this (the body owner is attacker-controlled); they go
// through updateOwned, which authorizes against the persisted user_id in the WHERE clause.
func (r *playerRepository) isPermitted(p *model.Player) bool {
u := loggedUser(r.ctx)
return u.IsAdmin || p.UserId == u.ID
}
func (r *playerRepository) Save(entity any) (string, error) {
t := entity.(*model.Player)
if !r.isPermitted(t) {
return "", rest.ErrPermissionDenied
}
return r.put("", t) // Save only creates; edits go through the owner-scoped Update
}
func (r *playerRepository) Update(id string, entity any, cols ...string) error {
t := entity.(*model.Player)
t.ID = id
return r.updateOwned(id, t, cols...)
}
func (r *playerRepository) Delete(id string) error {
return r.deleteOwned(id)
}
var _ model.PlayerRepository = (*playerRepository)(nil)
var _ rest.Repository = (*playerRepository)(nil)
var _ rest.Persistable = (*playerRepository)(nil)