Skip to content

Commit

Permalink
feat: file type validation (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
comoser authored Sep 26, 2023
1 parent 3691ae3 commit c5337af
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 6 deletions.
3 changes: 3 additions & 0 deletions packages/api/src/chats/chats.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RemoveDocumentFromChatRequestDto } from '@/chats/dtos/remove-document-f
import { ChatNotFoundExceptionSchema } from '@/chats/exceptions/chat-not-found.exception';
import { DocumentNotFoundExceptionSchema } from '@/chats/exceptions/document-not-found.exception';
import { DocumentPermissionsMismatchExceptionSchema } from '@/chats/exceptions/document-permissions-mismatch.exception';
import { DocumentTypeMismatchExceptionSchema } from '@/chats/exceptions/document-type-mismatch.exception';
import { MaxDocumentSizeLimitExceptionSchema } from '@/chats/exceptions/max-document-size-limit.exception';
import { FindChatByRoomIdUsecase } from '@/chats/usecases/find-chat-by-room-id.usecase';
import { FindChatMessageHistoryByRoomIdUsecase } from '@/chats/usecases/find-chat-message-history-by-room-id.usecase';
Expand Down Expand Up @@ -41,6 +42,7 @@ import {
ApiConflictResponse,
ApiConsumes,
ApiNoContentResponse,
ApiNotAcceptableResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
Expand Down Expand Up @@ -146,6 +148,7 @@ export class ChatsController {
@ApiNoContentResponse({ description: 'No content' })
@ApiBadRequestResponse({ schema: MaxDocumentSizeLimitExceptionSchema })
@ApiConflictResponse({ schema: DocumentPermissionsMismatchExceptionSchema })
@ApiNotAcceptableResponse({ schema: DocumentTypeMismatchExceptionSchema })
@ApiNotFoundResponse({ schema: ChatNotFoundExceptionSchema })
@ApiOperation({ description: 'Upload documents into a chat' })
uploadDocumentsToChat(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { HttpException, HttpStatus } from '@nestjs/common';

export const DocumentTypeMismatchExceptionSchema = {
type: 'object',
properties: {
statusCode: {
type: 'number',
example: 406,
},
message: {
type: 'array',
items: {
type: 'string',
example: 'document: type_mismatch',
},
},
error: {
type: 'string',
example: "Document type extension doesn't match its contents",
},
},
required: ['statusCode', 'message', 'error'],
};

export class DocumentTypeMismatchException extends HttpException {
constructor() {
super(
{
statusCode: HttpStatus.NOT_ACCEPTABLE,
message: ['document: type_mismatch'],
error: "Document type extension doesn't match its contents",
},
HttpStatus.NOT_ACCEPTABLE
);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { AiService } from '@/ai/facades/ai.service';
import { AppConfigService } from '@/app-config/app-config.service';
import { readFileSync } from 'fs';
import { ClerkAuthUserProvider } from '@/auth/providers/clerk/clerk-auth-user.provider';
import { ChatsRepository } from '@/chats/chats.repository';
import { ChatNotFoundException } from '@/chats/exceptions/chat-not-found.exception';
import { DocumentPermissionsMismatchException } from '@/chats/exceptions/document-permissions-mismatch.exception';
import { DocumentTypeMismatchException } from '@/chats/exceptions/document-type-mismatch.exception';
import { MaxDocumentSizeLimitException } from '@/chats/exceptions/max-document-size-limit.exception';
import { ACCEPTED_FILE_SIZE_LIMIT } from '@/common/constants/files';
import {
ACCEPTED_FILE_SIZE_LIMIT,
fileBufferSignatureByMimetypeMap,
} from '@/common/constants/files';
import { CHAT_DOCUMENT_UPLOAD_QUEUE } from '@/common/constants/queues';
import { createChatDocUploadJobFactory } from '@/common/jobs/chat-doc-upload.job';
import { Chat } from '@/common/types/chat';
Expand All @@ -21,9 +24,7 @@ export class UploadDocumentsToChatUsecase implements Usecase {
private readonly chatsRepository: ChatsRepository,
private readonly clerkAuthUserProvider: ClerkAuthUserProvider,
@InjectQueue(CHAT_DOCUMENT_UPLOAD_QUEUE)
private readonly chatDocUploadQueue: Queue,
private readonly appConfigService: AppConfigService,
private readonly aiService: AiService
private readonly chatDocUploadQueue: Queue
) {}

async execute(
Expand All @@ -39,6 +40,8 @@ export class UploadDocumentsToChatUsecase implements Usecase {

this.checkMaxDocumentsSizePerRoomInvariant(existingChat, files);

this.checkForValidDocumentTypesInvariant(files);

await this.checkDocumentRolesMatchUserRolesInvariant(
existingChat,
fileRoles
Expand Down Expand Up @@ -79,6 +82,22 @@ export class UploadDocumentsToChatUsecase implements Usecase {
}
}

private checkForValidDocumentTypesInvariant(files: Express.Multer.File[]) {
for (const file of files) {
const buffer = readFileSync(`${file.destination}/${file.filename}`);
const fileSignatureVerifier =
fileBufferSignatureByMimetypeMap[file.mimetype];
const fileSignature = buffer.toString(
'hex',
0,
fileSignatureVerifier.relevantBytes
);
if (!fileSignatureVerifier.signature.includes(fileSignature)) {
throw new DocumentTypeMismatchException();
}
}
}

private async checkDocumentRolesMatchUserRolesInvariant(
chat: Chat,
fileRoles: string[]
Expand Down
29 changes: 29 additions & 0 deletions packages/api/src/common/constants/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,35 @@ export const acceptedFileMimetypes = [
DOCX_MIMETYPE,
];

export const PDF_BUFFER_SIGNATURE = '25504446'; // 4 relevant bytes
export const TXT_UTF8_BUFFER_SIGNATURE = 'EFBBBF'; // 3 relevant bytes
export const TXT_BUFFER_SIGNATURE = '546869'; // 3 relevant bytes
export const DOCX_BUFFER_SIGNATURE = '504b0304'; // 4 relevant bytes

export const acceptedFileBufferSignatures = [
PDF_BUFFER_SIGNATURE,
TXT_UTF8_BUFFER_SIGNATURE,
TXT_BUFFER_SIGNATURE,
DOCX_BUFFER_SIGNATURE,
];

export const fileBufferSignatureByMimetypeMap: {
[key: string]: { relevantBytes: number; signature: string | string[] };
} = {
[PDF_MIMETYPE]: {
relevantBytes: 4,
signature: [PDF_BUFFER_SIGNATURE],
},
[TEXT_MIMETYPE]: {
relevantBytes: 3,
signature: [TXT_UTF8_BUFFER_SIGNATURE, TXT_BUFFER_SIGNATURE],
},
[DOCX_MIMETYPE]: {
relevantBytes: 4,
signature: [DOCX_BUFFER_SIGNATURE],
},
};

export function sanitizeFilename(filename: string): string {
const roomIdLength = 24;
const maxCollectionNameLength = 62;
Expand Down

0 comments on commit c5337af

Please sign in to comment.