From 80ca9aa9b6375c6818410c4af1243a20036a68bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 30 Jul 2026 00:27:39 +0000 Subject: [PATCH] soulseek: scope cached folder reads to their peer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probing the live routes turned up that /browse//dirs//files only checked the owner, not the peer: dir ids are global, so asking for one peer's folder id under a different peer's name returned the other peer's files with a 200. The UI always sends a matching pair so nothing misbehaved, but the URL was asserting a relationship the query never verified — a mismatched or stale request would show the wrong peer's contents rather than a 404. Co-Authored-By: Claude Opus 4.8 --- .../officer_db/src/queries/soulseek.ts | 18 ++++++++++++++++-- src/servers/sidecar/slskd/officer.ts | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/databases/officer_db/src/queries/soulseek.ts b/src/databases/officer_db/src/queries/soulseek.ts index b85f6ebd..0d250ecc 100644 --- a/src/databases/officer_db/src/queries/soulseek.ts +++ b/src/databases/officer_db/src/queries/soulseek.ts @@ -192,12 +192,26 @@ export async function getSoulseekBrowseDirs(params: BrowseDirPageParams): Promis } /** One cached folder's files. Scoped by user so a guessed dir id can't read another account's cache. */ -export async function getSoulseekBrowseDirFiles(userId: number, dirId: number): Promise { +type DirFilesParams = { userId: number; username: string; dirId: number }; + +// Joined and scoped by BOTH owner and peer: a dir id alone would happily return another peer's folder +// (ids are global), so the username in the request URL has to be part of the predicate, not decoration. +export async function getSoulseekBrowseDirFiles({ + userId, + username, + dirId, +}: DirFilesParams): Promise { const [row] = await db .select({ files: soulseekBrowseDirs.files }) .from(soulseekBrowseDirs) .innerJoin(soulseekBrowseSnapshots, eq(soulseekBrowseDirs.snapshotId, soulseekBrowseSnapshots.id)) - .where(and(eq(soulseekBrowseDirs.id, dirId), eq(soulseekBrowseSnapshots.userId, userId))) + .where( + and( + eq(soulseekBrowseDirs.id, dirId), + eq(soulseekBrowseSnapshots.userId, userId), + eq(soulseekBrowseSnapshots.username, username), + ), + ) .limit(1); return row ? (row.files as BrowsedFile[]) : null; } diff --git a/src/servers/sidecar/slskd/officer.ts b/src/servers/sidecar/slskd/officer.ts index 831ca6d9..e07994fa 100644 --- a/src/servers/sidecar/slskd/officer.ts +++ b/src/servers/sidecar/slskd/officer.ts @@ -127,7 +127,7 @@ async function handleBrowse({ req, url, userId, segments }: BrowseRouteParams): if (req.method !== 'GET') return methodNotAllowed(); const dirId = Number(segments[3]); if (!Number.isInteger(dirId) || dirId <= 0) return badRequest('invalid directory id'); - const files = await getSoulseekBrowseDirFiles(userId, dirId); + const files = await getSoulseekBrowseDirFiles({ userId, username, dirId }); return files ? Response.json(files) : notFound(); }