1
0
Files
bigcapital/packages/server/src/modules/Attachments/DeleteAttachment.ts
T
Ahmed Bouhuolia e18e61000d fix(server): prevent cross-tenant attachment access (IDOR)
Add tenant-scoped document lookup with throwIfNotFound() before S3
operations in GetAttachment, DeleteAttachment, and
GetAttachmentPresignedUrl services. This prevents users from reading,
deleting, or generating presigned URLs for attachments belonging to
other tenants.

Also adds RequirePermission decorators to the three attachment
endpoints and introduces Attachment ability subject with View and
Delete actions.

GHSA-rc4v-wq22-v6cf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:47:18 +02:00

57 lines
1.7 KiB
TypeScript

import { Inject, Injectable } from '@nestjs/common';
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { Knex } from 'knex';
import { ConfigService } from '@nestjs/config';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { S3_CLIENT } from '../S3/S3.module';
import { DocumentModel } from './models/Document.model';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { DocumentLinkModel } from './models/DocumentLink.model';
@Injectable()
export class DeleteAttachment {
constructor(
private readonly uow: UnitOfWork,
private readonly configService: ConfigService,
@Inject(S3_CLIENT)
private readonly s3Client: S3Client,
@Inject(DocumentModel.name)
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
@Inject(DocumentLinkModel.name)
private readonly documentLinkModel: TenantModelProxy<
typeof DocumentLinkModel
>,
) {}
/**
* Deletes the give file attachment file key.
* @param {string} filekey
*/
async delete(filekey: string): Promise<void> {
const foundDocument = await this.documentModel()
.query()
.findOne('key', filekey)
.throwIfNotFound();
const params = {
Bucket: this.configService.get('s3.bucket'),
Key: filekey,
};
await this.s3Client.send(new DeleteObjectCommand(params));
await this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Delete all document links
await this.documentLinkModel()
.query(trx)
.where('documentId', foundDocument.id)
.delete();
// Delete thedocument.
await this.documentModel().query(trx).findById(foundDocument.id).delete();
});
}
}