VYPR
High severityNVD Advisory· Published May 30, 2025· Updated May 30, 2025

Navidrome Transcoding Permission Bypass Vulnerability Report

CVE-2025-48948

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.

PackageAffected versionsPatched versions
github.com/navidrome/navidromeGo
< 0.56.00.56.0

Affected products

2

Patches

1
e5438552c63f

fix(transcoding): restrict transcoding operations to admin users (#4096)

https://github.com/navidrome/navidromeDeluan QuintãoMay 22, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.