diff --git a/src/backend/services/share/ShareConsistency.test.ts b/src/backend/services/share/ShareConsistency.test.ts index 42bccf9d9..f12702138 100644 --- a/src/backend/services/share/ShareConsistency.test.ts +++ b/src/backend/services/share/ShareConsistency.test.ts @@ -79,7 +79,10 @@ describe('share consistency across KV, SQL and Redis', () => { const found = await server.stores.user.getByUsername(username); if (!found) throw new Error('test user missing'); const email = `${username}@test.local`; - await server.stores.user.update(found.id, { email }); + await server.stores.user.update(found.id, { + email, + email_confirmed: true, + }); const user = await server.stores.user.getById(found.id, { force: true, }); diff --git a/src/backend/services/share/ShareService.test.ts b/src/backend/services/share/ShareService.test.ts index d9b4aea17..9c8deca3f 100644 --- a/src/backend/services/share/ShareService.test.ts +++ b/src/backend/services/share/ShareService.test.ts @@ -41,7 +41,10 @@ describe('ShareService', () => { const user = await server.stores.user.getByUsername(username); if (!user) throw new Error('test user missing'); const email = `${username}@test.local`; - await server.stores.user.update(user.id, { email }); + await server.stores.user.update(user.id, { + email, + email_confirmed: true, + }); const fresh = await server.stores.user.getById(user.id, { force: true, }); @@ -202,6 +205,33 @@ describe('ShareService', () => { ).rejects.toMatchObject({ statusCode: 400 }); }); + it('does not resolve an email its account has not confirmed', async () => { + const owner = await makeUser(); + const squatter = await makeUser(); + await server.stores.user.update(squatter.user.id, { + email_confirmed: false, + }); + const file = await makeFile(owner.user); + + await expect( + share(owner.actor, { + uid: file.uuid, + recipient: { email: squatter.email }, + mode: 'read', + }), + ).rejects.toMatchObject({ + statusCode: 404, + legacyCode: 'user_does_not_exist', + }); + + // A username names exactly one account, confirmed or not. + await share(owner.actor, { + uid: file.uuid, + recipient: { username: squatter.user.username }, + mode: 'read', + }); + }); + it('hides a file from a stranger trying to share it', async () => { const owner = await makeUser(); const stranger = await makeUser(); diff --git a/src/backend/services/share/ShareService.ts b/src/backend/services/share/ShareService.ts index 1b54dbb67..540c87cdf 100644 --- a/src/backend/services/share/ShareService.ts +++ b/src/backend/services/share/ShareService.ts @@ -661,7 +661,10 @@ export class ShareService extends PuterService { : username ? await this.stores.user.getByUsername(username) : null; - if (!user?.username) { + // An unconfirmed email is a claim, not an identity: resolving it would + // hand the share to whoever registered the address first. + const unconfirmedEmailMatch = Boolean(email) && !user?.email_confirmed; + if (!user?.username || unconfirmedEmailMatch) { throw new HttpError(404, 'Recipient does not exist', { legacyCode: 'user_does_not_exist', });