From 846cc0ce2bfa045173111fbc0770f0b2539f4e8c Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Tue, 18 Aug 2026 15:21:01 -0400 Subject: [PATCH] test: pin that a leaked uuid buys no access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A masked share path hides which folder an item sits in; it was never the thing deciding who may open it. Nothing checked that at the route level, so the guarantee rested on unit tests of the resolver alone. Reads one shared file through its masked path, then tries the sibling four ways: the shared uuid with the sibling's name, the sibling's own uuid, a `..` back out of the root, and the owner's real path. Worth knowing about this one: it's mutation-checked. Removing the head !== root.name guard in sharePathMask.ts fails it with reachable: /testuser/55dd54c0…/share-http-1df8933e.txt. I verified that specifically because two tests I wrote earlier in this chunk passed with their guards broken — both were vacuous, and I deleted them rather than commit false assurance. --- .../share/ShareController.http.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/backend/controllers/share/ShareController.http.test.ts b/src/backend/controllers/share/ShareController.http.test.ts index 39526349b..94b3c6be1 100644 --- a/src/backend/controllers/share/ShareController.http.test.ts +++ b/src/backend/controllers/share/ShareController.http.test.ts @@ -67,6 +67,48 @@ describe('share endpoints over HTTP', () => { return { uid, path, name }; }; + // Masking hides where an item sits, not who may open it. + it('will not let a masked path reach an unshared sibling', async () => { + const owner = env.users.user; + const recipient = env.users.other; + const shared = await makeFile(owner); + const secret = await makeFile(owner); + + await post('/share', owner.token, { + recipients: [recipient.username], + items: [{ uid: shared.uid }], + mode: 'read', + }); + + const read = (path: string) => + fetch( + `${env.apiOrigin}/read?${new URLSearchParams({ file: path })}`, + { + headers: { + authorization: `Bearer ${recipient.token}`, + origin: env.apiOrigin, + }, + }, + ); + + const masked = `/${owner.username}/${shared.uid}/${shared.name}`; + expect((await read(masked)).status).toBe(200); + + for (const attempt of [ + // The shared item's root, renamed to the sibling. + `/${owner.username}/${shared.uid}/${secret.name}`, + // The sibling's own uuid, as if it had leaked. + `/${owner.username}/${secret.uid}/${secret.name}`, + // Back out of the root the uuid vouched for. + `/${owner.username}/${shared.uid}/${shared.name}/../${secret.name}`, + // The owner's real path, named outright. + secret.path, + ]) { + const res = await read(attempt); + expect(res.status, `reachable: ${attempt}`).not.toBe(200); + } + }); + it('shares an item, lists it for the recipient, then revokes it', async () => { const owner = env.users.user; const recipient = env.users.other;