From 952f1fbad34a9ca95200da91045512a1a94d8265 Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Thu, 25 Jun 2026 15:54:29 +0800 Subject: [PATCH] fix(guest): deny operator personal-data reads to guests GET /recommend/songs, /personal/fm, and /user/playlists read the operator's own logged-in music account; gate them with requireNotGuest so login-less guests cannot see the operator's recommendations, FM, or playlists. Generic search/browse stays open. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/web/api/music.ts | 6 +++--- src/web/api/permissions-enforcement.test.ts | 23 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/web/api/music.ts b/src/web/api/music.ts index 3a0f682..d4446e7 100644 --- a/src/web/api/music.ts +++ b/src/web/api/music.ts @@ -128,7 +128,7 @@ export function createMusicRouter( } }); - router.get("/recommend/songs", async (req, res) => { + router.get("/recommend/songs", requireNotGuest, async (req, res) => { try { const provider = getProvider(req.query.platform as string); if (!provider.getDailyRecommendSongs) { @@ -143,7 +143,7 @@ export function createMusicRouter( } }); - router.get("/personal/fm", async (req, res) => { + router.get("/personal/fm", requireNotGuest, async (req, res) => { try { const provider = getProvider(req.query.platform as string); if (!provider.getPersonalFm) { @@ -158,7 +158,7 @@ export function createMusicRouter( } }); - router.get("/user/playlists", async (req, res) => { + router.get("/user/playlists", requireNotGuest, async (req, res) => { try { const provider = getProvider(req.query.platform as string); if (!provider.getUserPlaylists) { diff --git a/src/web/api/permissions-enforcement.test.ts b/src/web/api/permissions-enforcement.test.ts index c669cdb..f74a7bf 100644 --- a/src/web/api/permissions-enforcement.test.ts +++ b/src/web/api/permissions-enforcement.test.ts @@ -199,6 +199,29 @@ describe("permission enforcement on action routes", () => { }); }); + // The operator's personal-account reads (their recommendations, FM, and + // playlists) must never leak to login-less guests. These routes are gated + // with requireNotGuest; generic search/browse stays open. + describe("operator personal-data reads are denied to guests", () => { + const personalRoutes = [ + "/api/music/recommend/songs", + "/api/music/personal/fm", + "/api/music/user/playlists", + ]; + + for (const route of personalRoutes) { + it(`GET ${route} is 403 for a guest`, async () => { + const app = makeApp(guest()); + expect((await request(app).get(route)).status).toBe(403); + }); + + it(`GET ${route} is NOT 403 for a member`, async () => { + const app = makeApp(member([], "all")); + expect((await request(app).get(route)).status).not.toBe(403); + }); + } + }); + describe("read-only routes stay open", () => { it("GET /api/auth/status not gated", async () => { const app = makeApp(member([], "all"));