Skip to content

Commit 0ff5937

Browse files
authored
feat(backend): add security check (#163)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced file upload validation for project photos, ensuring only allowed image formats (JPEG, PNG, WebP) are accepted and that files do not exceed 5MB. - **Refactor** - Streamlined file handling during photo updates by centralizing validation logic, resulting in clearer error handling and improved reliability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent f36bafc commit 0ff5937

File tree

2 files changed

+61
-8
lines changed

2 files changed

+61
-8
lines changed
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { BadRequestException } from '@nestjs/common';
2+
import { FileUpload } from 'graphql-upload-minimal';
3+
import path from 'path';
4+
5+
/** Maximum allowed file size (5MB) */
6+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
7+
8+
/** Allowed image MIME types */
9+
const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
10+
11+
/** Allowed file extensions */
12+
const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp'];
13+
14+
/**
15+
* Validates a file upload (size, type) and returns a Buffer.
16+
* @param file - FileUpload object from GraphQL
17+
* @returns Promise<Buffer> - The file data in buffer format
18+
* @throws BadRequestException - If validation fails
19+
*/
20+
export async function validateAndBufferFile(
21+
file: FileUpload,
22+
): Promise<{ buffer: Buffer; mimetype: string }> {
23+
const { filename, createReadStream, mimetype } = await file;
24+
25+
// Extract the file extension
26+
const extension = path.extname(filename).toLowerCase();
27+
28+
// Validate MIME type
29+
if (!ALLOWED_MIME_TYPES.includes(mimetype)) {
30+
throw new BadRequestException(
31+
`Invalid file type: ${mimetype}. Only JPEG, PNG, and WebP are allowed.`,
32+
);
33+
}
34+
35+
// Validate file extension
36+
if (!ALLOWED_EXTENSIONS.includes(extension)) {
37+
throw new BadRequestException(
38+
`Invalid file extension: ${extension}. Only .jpg, .jpeg, .png, and .webp are allowed.`,
39+
);
40+
}
41+
42+
const chunks: Buffer[] = [];
43+
let fileSize = 0;
44+
45+
// Read file stream and check size
46+
for await (const chunk of createReadStream()) {
47+
fileSize += chunk.length;
48+
if (fileSize > MAX_FILE_SIZE) {
49+
throw new BadRequestException(
50+
'File size exceeds the maximum allowed limit (5MB).',
51+
);
52+
}
53+
chunks.push(chunk);
54+
}
55+
56+
return { buffer: Buffer.concat(chunks), mimetype };
57+
}

backend/src/project/project.resolver.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ProjectGuard } from '../guard/project.guard';
2222
import { GetUserIdFromToken } from '../decorator/get-auth-token.decorator';
2323
import { Chat } from 'src/chat/chat.model';
2424
import { User } from 'src/user/user.model';
25+
import { validateAndBufferFile } from 'src/common/security/file_check';
2526

2627
@Resolver(() => Project)
2728
export class ProjectsResolver {
@@ -98,14 +99,8 @@ export class ProjectsResolver {
9899
this.logger.log(`User ${userId} uploading photo for project ${projectId}`);
99100

100101
// Extract the file data
101-
const { createReadStream, mimetype } = await file;
102-
103-
// Buffer the file content
104-
const chunks = [];
105-
for await (const chunk of createReadStream()) {
106-
chunks.push(chunk);
107-
}
108-
const buffer = Buffer.concat(chunks);
102+
// Validate file and convert it to buffer
103+
const { buffer, mimetype } = await validateAndBufferFile(file);
109104

110105
// Call the service with the extracted buffer and mimetype
111106
return this.projectService.updateProjectPhotoUrl(
@@ -115,6 +110,7 @@ export class ProjectsResolver {
115110
mimetype,
116111
);
117112
}
113+
118114
@Mutation(() => Project)
119115
async updateProjectPublicStatus(
120116
@GetUserIdFromToken() userId: string,

0 commit comments

Comments
 (0)