mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 07:27:04 +00:00
fix(share): authorize before resolving the recipient
share() looked up the recipient in parallel with the entry, before the manage check — and the two failures carried different error codes. Any verified user with a real entry uid could probe arbitrary emails and usernames for account existence, at no quota cost. Resolve the entry, authorize, and only then resolve the recipient: an unauthorized caller now sees the identical safe 404 whether or not the recipient exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5f440333cd
commit
e47ec447b0
@@ -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();
|
||||
|
||||
@@ -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', {
|
||||
|
||||
Reference in New Issue
Block a user