From 648f2bd0eafc709489b78b42c20ee8b64fdbfce2 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 20 Aug 2026 11:52:07 -0700 Subject: [PATCH 1/2] fix(api): stop signing media links for non-streamable tracks #1023 guarded the stream and download endpoints, but that only closed two routes. `dbv1.TracksKeyed` builds the Stream/Download/Preview media links with no reference to `is_streamable`, so `/v1/tracks/{id}` and `/v1/resolve` keep handing out a signed content-node URL for tracks whose owner is deactivated or delisted. Verified against the delisted account from the original report: the signed URL from the track response served the full mp3. Hoist the is_streamable expression above the media-link block and gate all three on it, matching the empty-cid case directly above, which already leaves the link nil and lets the endpoints report the track as unavailable. Preview is included because a preview clip is still the artist's audio. Signatures expire after 48h on the content node, so URLs already handed out age out on their own - the leak is that the API mints a fresh one per request. Co-Authored-By: Claude Opus 5 --- api/dbv1/tracks.go | 29 ++++++++++---- api/v1_track_test.go | 94 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/api/dbv1/tracks.go b/api/dbv1/tracks.go index 53fcd874..90eaf33b 100644 --- a/api/dbv1/tracks.go +++ b/api/dbv1/tracks.go @@ -196,13 +196,26 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32] } } + // A track is streamable unless it was deleted or its owner is no longer + // active - either the artist deactivated their own account or the + // account was delisted by the trusted notifier. + isStreamable := !rawTrack.IsDelete && !user.IsDeactivated + + // Two reasons to leave a media link nil, both with the same effect: the + // URL should never be handed out, and the endpoints report the track as + // unavailable instead. + // // A track row can have empty cid columns (e.g. an upload-v2 row whose - // track_cid/orig_file_cid backfill never ran). Signing an empty cid - // produces a content-node URL that is guaranteed to 404, so leave the - // media link nil instead and let the endpoints report the track as - // unavailable. + // track_cid/orig_file_cid backfill never ran), and signing an empty cid + // produces a content-node URL that is guaranteed to 404. + // + // A non-streamable track is worse: the cid is real, so the signed URL + // works. The stream and download endpoints reject these, but that only + // closes those two routes - anyone reading the track response could + // still fetch the audio straight from the content node. Preview is + // included because a preview clip is still the artist's audio. var stream *MediaLink - if access.Stream && rawTrack.TrackCid.String != "" { + if isStreamable && access.Stream && rawTrack.TrackCid.String != "" { stream, err = mediaLink(rawTrack.TrackCid.String, rawTrack.TrackID, arg.MyID.(int32), id3Tags) if err != nil { return nil, err @@ -210,7 +223,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32] } var download *MediaLink - if rawTrack.IsDownloadable && access.Download { + if isStreamable && rawTrack.IsDownloadable && access.Download { cid := rawTrack.OrigFileCid.String if cid == "" { cid = rawTrack.TrackCid.String @@ -224,7 +237,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32] } var preview *MediaLink - if rawTrack.PreviewCid.String != "" { + if isStreamable && rawTrack.PreviewCid.String != "" { preview, err = mediaLink(rawTrack.PreviewCid.String, rawTrack.TrackID, arg.MyID.(int32), id3Tags) if err != nil { return nil, err @@ -233,7 +246,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32] track := Track{ GetTracksRow: rawTrack, - IsStreamable: !rawTrack.IsDelete && !user.IsDeactivated, + IsStreamable: isStreamable, Permalink: fmt.Sprintf("/%s/%s", user.Handle.String, rawTrack.Slug.String), Artwork: squareImageStruct(rawTrack.CoverArtSizes, rawTrack.CoverArt), Stream: stream, diff --git a/api/v1_track_test.go b/api/v1_track_test.go index e9aa1422..ba503193 100644 --- a/api/v1_track_test.go +++ b/api/v1_track_test.go @@ -4,6 +4,7 @@ import ( "testing" "api.audius.co/api/dbv1" + "api.audius.co/database" "api.audius.co/trashid" "github.com/stretchr/testify/assert" ) @@ -18,8 +19,8 @@ func TestGetTrack(t *testing.T) { assert.Equal(t, 200, status) jsonAssert(t, body, map[string]any{ - "data.id": "eYJyn", - "data.title": "Culca Canyon", + "data.id": "eYJyn", + "data.title": "Culca Canyon", "data.play_count": 0, }) } @@ -40,17 +41,17 @@ func TestGetTrackPersonalization(t *testing.T) { // repost/save flags should be false. _, body := testGet(t, app, "/v1/full/tracks/eYJyn?user_id=ML51L", &resp) jsonAssert(t, body, map[string]any{ - "data.id": "eYJyn", - "data.has_current_user_reposted": false, - "data.has_current_user_saved": false, + "data.id": "eYJyn", + "data.has_current_user_reposted": false, + "data.has_current_user_saved": false, "data.followee_reposts.0.user_id": trashid.MustEncodeHashID(1), }) _, body = testGet(t, app, "/v1/full/tracks/eYZmn?user_id=ML51L", &resp) jsonAssert(t, body, map[string]any{ - "data.id": "eYZmn", - "data.has_current_user_reposted": false, - "data.has_current_user_saved": false, + "data.id": "eYZmn", + "data.has_current_user_reposted": false, + "data.has_current_user_saved": false, "data.followee_favorites.0.user_id": trashid.MustEncodeHashID(1), }) @@ -173,3 +174,80 @@ func TestGetTrackUsdcPurchaseSelfAccess(t *testing.T) { "data.access.download": true, }) } + +// A track whose owner is no longer active - the artist deactivated their own +// account, or the account was delisted by the trusted notifier - must not carry +// signed content-node URLs in its response. The stream and download endpoints +// already reject these, but the media links in the track response bypass those +// endpoints entirely: the cid is real, so the signed URL serves the full audio +// straight from the content node to anyone who reads the response. +func TestGetTrack_NonStreamableOmitsMediaLinks(t *testing.T) { + for _, tc := range []struct { + name string + track map[string]any + user map[string]any + }{ + { + name: "deactivated owner", + track: map[string]any{ + "track_id": 1, "owner_id": 1, "title": "Deactivated Owner", + "track_cid": "QmTrackCid", "orig_file_cid": "QmOrigCid", + "preview_cid": "QmPreviewCid", "is_downloadable": true, + }, + user: map[string]any{"user_id": 1, "handle": "testuser1", "is_deactivated": true}, + }, + { + name: "deleted track", + track: map[string]any{ + "track_id": 1, "owner_id": 1, "title": "Deleted", + "track_cid": "QmTrackCid", "orig_file_cid": "QmOrigCid", + "preview_cid": "QmPreviewCid", "is_downloadable": true, + "is_delete": true, + }, + user: map[string]any{"user_id": 1, "handle": "testuser1"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + app := emptyTestApp(t) + database.Seed(app.pool.Replicas[0], database.FixtureMap{ + "tracks": []map[string]any{tc.track}, + "users": []map[string]any{tc.user}, + }) + + var resp struct{ Data dbv1.Track } + status, _ := testGet(t, app, "/v1/full/tracks/"+trashid.MustEncodeHashID(1), &resp) + assert.Equal(t, 200, status) + + assert.False(t, resp.Data.IsStreamable) + assert.Nil(t, resp.Data.Stream) + assert.Nil(t, resp.Data.Download) + assert.Nil(t, resp.Data.Preview) + }) + } +} + +// The guard above is scoped to non-streamable tracks: an ordinary track with an +// active owner must still get its signed media links. +func TestGetTrack_StreamableKeepsMediaLinks(t *testing.T) { + app := emptyTestApp(t) + database.Seed(app.pool.Replicas[0], database.FixtureMap{ + "tracks": []map[string]any{ + { + "track_id": 1, "owner_id": 1, "title": "Active Owner", + "track_cid": "QmTrackCid", "orig_file_cid": "QmOrigCid", + "preview_cid": "QmPreviewCid", "is_downloadable": true, + }, + }, + "users": []map[string]any{{"user_id": 1, "handle": "testuser1"}}, + }) + + var resp struct{ Data dbv1.Track } + status, _ := testGet(t, app, "/v1/full/tracks/"+trashid.MustEncodeHashID(1), &resp) + assert.Equal(t, 200, status) + + assert.True(t, resp.Data.IsStreamable) + assert.NotNil(t, resp.Data.Stream) + assert.NotNil(t, resp.Data.Download) + assert.NotNil(t, resp.Data.Preview) + assert.Contains(t, resp.Data.Stream.Url, "signature=") +} From 0e31fb4539f0360c0b4d7b4d709e03298f09bb2a Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Fri, 21 Aug 2026 16:02:53 -0700 Subject: [PATCH 2/2] ci: retrigger (dependency install step flaked)