Navidrome Transcoding Permission Bypass Vulnerability Report
Description
Navidrome is an open source web-based music collection server and streamer. A permission verification flaw in versions prior to 0.56.0 allows any authenticated regular user to bypass authorization checks and perform administrator-only transcoding configuration operations, including creating, modifying, and deleting transcoding settings. In the threat model where administrators are trusted but regular users are not, this vulnerability represents a significant security risk when transcoding is enabled. Version 0.56.0 patches the issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Navidrome versions prior to 0.56.0 allow authenticated non-admin users to create, modify, delete, or list transcoding configurations, bypassing intended admin-only authorization.
Vulnerability
A permission verification flaw exists in Navidrome's transcoding API, affecting versions before 0.56.0 [1]. The server fails to properly verify administrative privileges when handling requests to endpoints such as POST /api/transcoding, PUT /api/transcoding/:id, DELETE /api/transcoding/:id, and GET /api/transcoding [3]. This allows any authenticated regular user, regardless of their JWT role claim ("adm":false), to bypass authorization checks [3].
Exploitation
To exploit the vulnerability, an attacker must have a valid Navidrome user account and the server must have transcoding enabled (though disabled by default) [1][3]. The attacker sends crafted HTTP requests to the API endpoints, manipulating transcoding settings [3]. A Proof of Concept (PoC) demonstrates that a non-admin user can create or modify transcoding configurations, including setting a custom command and bitrate [3]. The missing authorization check is in the repository layer, as the fix introduces explicit admin-user verification in TranscodingRepository [2].
Impact
A successful attack allows a regular user to perform administrator-only operations on transcoding settings [1]. This could lead to unauthorized changes to server transcoding behavior, potentially causing resource abuse, service disruption, or unexpected audio processing [1][3]. The vulnerability is considered significant in environments where admin users are trusted but regular users are not, particularly when transcoding is active [1].
Mitigation
The issue is patched in Navidrome version 0.56.0 [1]. Users should upgrade immediately to restrict transcoding configuration operations to admin users only [1][2]. The commit fix introduces role-based checks by injecting the user context into the repository, ensuring only users with admin privileges can access transcoding management functions [2].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/navidrome/navidromeGo | < 0.56.0 | 0.56.0 |
Affected products
2- navidrome/navidromev5Range: < 0.56.0
Patches
1e5438552c63ffix(transcoding): restrict transcoding operations to admin users (#4096)
3 files changed · +113 −0
persistence/sql_base_repository.go+5 −0 modified@@ -65,6 +65,11 @@ func loggedUser(ctx context.Context) *model.User { } } +func isAdmin(ctx context.Context) bool { + user := loggedUser(ctx) + return user.IsAdmin +} + func (r *sqlRepository) registerModel(instance any, filters map[string]filterFunc) { if r.tableName == "" { r.tableName = strings.TrimPrefix(reflect.TypeOf(instance).String(), "*model.")
persistence/transcoding_repository.go+12 −0 modified@@ -41,6 +41,9 @@ func (r *transcodingRepository) FindByFormat(format string) (*model.Transcoding, } func (r *transcodingRepository) Put(t *model.Transcoding) error { + if !isAdmin(r.ctx) { + return rest.ErrPermissionDenied + } _, err := r.put(t.ID, t) return err } @@ -69,6 +72,9 @@ func (r *transcodingRepository) NewInstance() interface{} { } func (r *transcodingRepository) Save(entity interface{}) (string, error) { + if !isAdmin(r.ctx) { + return "", rest.ErrPermissionDenied + } t := entity.(*model.Transcoding) id, err := r.put(t.ID, t) if errors.Is(err, model.ErrNotFound) { @@ -78,6 +84,9 @@ func (r *transcodingRepository) Save(entity interface{}) (string, error) { } func (r *transcodingRepository) Update(id string, entity interface{}, cols ...string) error { + if !isAdmin(r.ctx) { + return rest.ErrPermissionDenied + } t := entity.(*model.Transcoding) t.ID = id _, err := r.put(id, t) @@ -88,6 +97,9 @@ func (r *transcodingRepository) Update(id string, entity interface{}, cols ...st } func (r *transcodingRepository) Delete(id string) error { + if !isAdmin(r.ctx) { + return rest.ErrPermissionDenied + } err := r.delete(Eq{"id": id}) if errors.Is(err, model.ErrNotFound) { return rest.ErrNotFound
persistence/transcoding_repository_test.go+96 −0 added@@ -0,0 +1,96 @@ +package persistence + +import ( + "github.com/deluan/rest" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/request" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("TranscodingRepository", func() { + var repo model.TranscodingRepository + var adminRepo model.TranscodingRepository + + BeforeEach(func() { + ctx := log.NewContext(GinkgoT().Context()) + ctx = request.WithUser(ctx, regularUser) + repo = NewTranscodingRepository(ctx, GetDBXBuilder()) + + adminCtx := log.NewContext(GinkgoT().Context()) + adminCtx = request.WithUser(adminCtx, adminUser) + adminRepo = NewTranscodingRepository(adminCtx, GetDBXBuilder()) + }) + + AfterEach(func() { + // Clean up any transcoding created during the tests + tc, err := adminRepo.FindByFormat("test_format") + if err == nil { + err = adminRepo.(*transcodingRepository).Delete(tc.ID) + Expect(err).ToNot(HaveOccurred()) + } + }) + + Describe("Admin User", func() { + It("creates a new transcoding", func() { + base, err := adminRepo.CountAll() + Expect(err).ToNot(HaveOccurred()) + + err = adminRepo.Put(&model.Transcoding{ID: "new", Name: "new", TargetFormat: "test_format", DefaultBitRate: 320, Command: "ffmpeg"}) + Expect(err).ToNot(HaveOccurred()) + + count, err := adminRepo.CountAll() + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(base + 1)) + }) + + It("updates an existing transcoding", func() { + tr := &model.Transcoding{ID: "upd", Name: "old", TargetFormat: "test_format", DefaultBitRate: 100, Command: "ffmpeg"} + Expect(adminRepo.Put(tr)).To(Succeed()) + tr.Name = "updated" + err := adminRepo.Put(tr) + Expect(err).ToNot(HaveOccurred()) + res, err := adminRepo.FindByFormat("test_format") + Expect(err).ToNot(HaveOccurred()) + Expect(res.Name).To(Equal("updated")) + }) + + It("deletes a transcoding", func() { + err := adminRepo.Put(&model.Transcoding{ID: "to-delete", Name: "temp", TargetFormat: "test_format", DefaultBitRate: 256, Command: "ffmpeg"}) + Expect(err).ToNot(HaveOccurred()) + err = adminRepo.(*transcodingRepository).Delete("to-delete") + Expect(err).ToNot(HaveOccurred()) + _, err = adminRepo.Get("to-delete") + Expect(err).To(MatchError(model.ErrNotFound)) + }) + }) + + Describe("Regular User", func() { + It("fails to create", func() { + err := repo.Put(&model.Transcoding{ID: "bad", Name: "bad", TargetFormat: "test_format", DefaultBitRate: 64, Command: "ffmpeg"}) + Expect(err).To(Equal(rest.ErrPermissionDenied)) + }) + + It("fails to update", func() { + tr := &model.Transcoding{ID: "updreg", Name: "old", TargetFormat: "test_format", DefaultBitRate: 64, Command: "ffmpeg"} + Expect(adminRepo.Put(tr)).To(Succeed()) + + tr.Name = "bad" + err := repo.Put(tr) + Expect(err).To(Equal(rest.ErrPermissionDenied)) + + //_ = adminRepo.(*transcodingRepository).Delete("updreg") + }) + + It("fails to delete", func() { + tr := &model.Transcoding{ID: "delreg", Name: "temp", TargetFormat: "test_format", DefaultBitRate: 64, Command: "ffmpeg"} + Expect(adminRepo.Put(tr)).To(Succeed()) + + err := repo.(*transcodingRepository).Delete("delreg") + Expect(err).To(Equal(rest.ErrPermissionDenied)) + + //_ = adminRepo.(*transcodingRepository).Delete("delreg") + }) + }) +})
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-f238-rggp-82m3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-48948ghsaADVISORY
- github.com/navidrome/navidrome/commit/e5438552c63fecb6284e1b179dddae91ede869c8ghsax_refsource_MISCWEB
- github.com/navidrome/navidrome/pull/4096ghsax_refsource_MISCWEB
- github.com/navidrome/navidrome/security/advisories/GHSA-f238-rggp-82m3ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.