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 path from 'path';
import { import {
ForbiddenException,
Inject, Inject,
Injectable, Injectable,
NotFoundException, 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. * 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 token The signed token used to verify and locate the attachment.
* @param req - The HTTP express request object. * @param req - The HTTP express request object.
* @return A streamable file of the attachment. * @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 { try {
const { const {
exp: _exp, exp: _exp,
@ -336,6 +349,15 @@ export default abstract class ChannelHandler<
...result ...result
} = this.jwtService.verify(token, this.jwtSignOptions); } = this.jwtService.verify(token, this.jwtSignOptions);
const attachment = plainToClass(Attachment, result); 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); return await this.attachmentService.download(attachment);
} catch (err) { } catch (err) {
this.logger.error('Failed to download attachment', err); this.logger.error('Failed to download attachment', err);

View File

@ -1345,4 +1345,44 @@ export default abstract class BaseWebChannelHandler<
}; };
return subscriber; 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;
}
}
} }