diff --git a/src/backend/services/share/ShareService.test.ts b/src/backend/services/share/ShareService.test.ts index f8aad82c7..d9b4aea17 100644 --- a/src/backend/services/share/ShareService.test.ts +++ b/src/backend/services/share/ShareService.test.ts @@ -219,6 +219,33 @@ describe('ShareService', () => { ).rejects.toMatchObject({ statusCode: 404 }); }); + it('tells a stranger nothing about whether a recipient account exists', async () => { + const owner = await makeUser(); + const stranger = await makeUser(); + const recipient = await makeUser(); + const file = await makeFile(owner.user); + + // The recipient must resolve only after authorization: a caller who + // cannot manage the entry gets the same "no such subject" error for a + // real recipient and a made-up one, so /share cannot be used to probe + // which emails have accounts. + const probe = (email: string) => + share(stranger.actor, { + uid: file.uuid, + recipient: { email }, + mode: 'read', + }); + + await expect(probe(recipient.email)).rejects.toMatchObject({ + statusCode: 404, + legacyCode: 'subject_does_not_exist', + }); + await expect(probe('nobody@nowhere.test')).rejects.toMatchObject({ + statusCode: 404, + legacyCode: 'subject_does_not_exist', + }); + }); + it('revokes access and drops the index row', async () => { const owner = await makeUser(); const recipient = await makeUser(); diff --git a/src/backend/services/share/ShareService.ts b/src/backend/services/share/ShareService.ts index cc5e87034..1b54dbb67 100644 --- a/src/backend/services/share/ShareService.ts +++ b/src/backend/services/share/ShareService.ts @@ -203,13 +203,13 @@ export class ShareService extends PuterService { const issuerId = this.#requireUserId(actor); const mode = this.#requireMode(input.mode); - // Independent reads; the authorization check needs only the entry. - const [entry, holder] = await Promise.all([ - this.#resolveEntry(input), - this.#resolveRecipient(input.recipient), - ]); - + // Authorization before recipient resolution: a caller who cannot + // manage the entry must learn nothing from this endpoint — including + // whether an email or username has an account. "Recipient does not + // exist" may only be observed by someone entitled to share. + const entry = await this.#resolveEntry(input); await this.#assertCanManage(actor, entry); + const holder = await this.#resolveRecipient(input.recipient); if (holder.id === issuerId) { throw new HttpError(400, 'cannot share with yourself', {