CVE-2026-49340: gonic Arbitrary File Write via Path Traversal in createPlaylist
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.
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
# 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).