feat: enforce security to access own attachment

This commit is contained in:
Mohamed Marrouchi 2025-01-16 17:25:16 +01:00
parent c27f37a6e6
commit 4fac5d4fc9
2 changed files with 64 additions and 2 deletions

View File

@ -9,6 +9,7 @@
import path from 'path';
import {
ForbiddenException,
Inject,
Injectable,
NotFoundException,
@ -316,6 +317,19 @@ export default abstract class ChannelHandler<
}
}
/**
* Checks if the request is authorized to download a given attachment file.
* Can be overriden by the channel handler to customize, by default it shouldn't
* allow any client to download a subscriber attachment for example.
*
* @param attachment The attachment object
* @param req - The HTTP express request object.
* @return True, if requester is authorized to download the attachment
*/
public async hasDownloadAccess(attachment: Attachment, _req: Request) {
return attachment.access === 'public';
}
/**
* Downloads an attachment using a signed token.
*
@ -326,9 +340,8 @@ export default abstract class ChannelHandler<
* @param token The signed token used to verify and locate the attachment.
* @param req - The HTTP express request object.
* @return A streamable file of the attachment.
* @throws NotFoundException if the attachment cannot be found or the token is invalid.
*/
public async download(token: string, _req: Request) {
public async download(token: string, req: Request) {
try {
const {
exp: _exp,
@ -336,6 +349,15 @@ export default abstract class ChannelHandler<
...result
} = this.jwtService.verify(token, this.jwtSignOptions);
const attachment = plainToClass(Attachment, result);
// Check access
const canDownload = await this.hasDownloadAccess(attachment, req);
if (!canDownload) {
throw new ForbiddenException(
'You are not authorized to download the attachment',
);
}
return await this.attachmentService.download(attachment);
} catch (err) {
this.logger.error('Failed to download attachment', err);

View File

@ -1345,4 +1345,44 @@ export default abstract class BaseWebChannelHandler<
};
return subscriber;
}
/**
* Checks if the request is authorized to download a given attachment file.
*
* @param attachment The attachment object
* @param req - The HTTP express request object.
* @return True, if requester is authorized to download the attachment
*/
public async hasDownloadAccess(attachment: Attachment, req: Request) {
const subscriberId = req.session?.web?.profile?.id as string;
if (attachment.access === 'public') {
return true;
} else if (!subscriberId) {
this.logger.warn(
`Unauthorized access attempt to attachment ${attachment.id}`,
);
return false;
} else if (
attachment.createdByRef === 'Subscriber' &&
subscriberId === attachment.createdBy
) {
// Either subscriber wants to access the attachment he sent
return true;
} else {
// Or, he would like to access an attachment sent to him privately
const message = await this.messageService.findOne({
['recipient' as any]: subscriberId,
$or: [
{ 'message.attachment.payload.id': attachment.id },
{
'message.attachment': {
$elemMatch: { 'payload.id': attachment.id },
},
},
],
});
return !!message;
}
}
}