Skip to content

Commit dc2228a

Browse files
authored
Merge pull request #111 from SOURAV-ROY/sentinel/file-upload-extension-validation-8550321914153817534
🛡️ Sentinel: Fix file upload extension validation in bootcamp photo upload
2 parents 486752e + 9dad58d commit dc2228a

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

controllers/bootcampsController.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,18 @@ exports.bootcampPhotoUpload = asyncHandler(async (req, res, next) => {
213213
return next(new ErrorResponse(`Please Upload An Image File`, 400));
214214
}
215215

216+
// Ensure file extension is an allowed image extension to prevent arbitrary file upload vulnerabilities
217+
const ext = path.parse(file.name).ext.toLowerCase();
218+
const allowedExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
219+
if (!allowedExtensions.includes(ext)) {
220+
return next(
221+
new ErrorResponse(
222+
`Please Upload A Valid Image File Extension (.jpg, .jpeg, .png, .gif, .webp)`,
223+
400,
224+
),
225+
);
226+
}
227+
216228
//Check File Size *************************************************************************
217229
if (file.size > process.env.MAX_FILE_UPLOAD) {
218230
return next(
@@ -224,7 +236,7 @@ exports.bootcampPhotoUpload = asyncHandler(async (req, res, next) => {
224236
}
225237

226238
//Create Custom FileName*******************************************************************
227-
file.name = `photo_${bootcamp._id}${path.parse(file.name).ext}`;
239+
file.name = `photo_${bootcamp._id}${ext}`;
228240
await file.mv(
229241
`${process.env.FILE_UPLOAD_PATH}/${file.name}`,
230242
async (error) => {

tests/mongo_sanitize.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe("NoSQL Query Injection Prevention Middleware", () => {
1313
.post("/api/v1/auth/login")
1414
.set("x-csrf-token", csrfToken)
1515
.send({
16-
email: { "$gt": "" },
16+
email: { $gt: "" },
1717
password: "password123",
1818
});
1919

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const { bootcampPhotoUpload } = require("../controllers/bootcampsController");
2+
const { Bootcamp } = require("../models");
3+
const ErrorResponse = require("../utils/ErrorResponse");
4+
5+
jest.mock("../models", () => ({
6+
Bootcamp: {
7+
findById: jest.fn(),
8+
findByIdAndUpdate: jest.fn().mockResolvedValue({}),
9+
},
10+
}));
11+
12+
describe("Bootcamp Photo Upload File Extension Validation", () => {
13+
let req, res, next;
14+
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
req = {
18+
params: { id: "60d5ec49f1b2c80015f8e001" },
19+
user: {
20+
id: "5d7a514b5d2c12c7449be042",
21+
role: "publisher",
22+
name: "Publisher User",
23+
},
24+
files: null,
25+
};
26+
res = {
27+
status: jest.fn().mockReturnThis(),
28+
json: jest.fn(),
29+
};
30+
next = jest.fn();
31+
});
32+
33+
it("should reject files with invalid extensions (e.g. .php, .html, .svg) even if mimetype is image/png", async () => {
34+
Bootcamp.findById.mockResolvedValue({
35+
_id: "60d5ec49f1b2c80015f8e001",
36+
user: { toString: () => "5d7a514b5d2c12c7449be042" },
37+
});
38+
39+
req.files = {
40+
file: {
41+
name: "malicious.php",
42+
mimetype: "image/png", // Forged MIME type
43+
size: 1000,
44+
},
45+
};
46+
47+
await bootcampPhotoUpload(req, res, next);
48+
49+
expect(next).toHaveBeenCalledTimes(1);
50+
const error = next.mock.calls[0][0];
51+
expect(error).toBeInstanceOf(ErrorResponse);
52+
expect(error.statusCode).toBe(400);
53+
expect(error.message).toContain(
54+
"Please Upload A Valid Image File Extension",
55+
);
56+
});
57+
58+
it("should accept valid image extension .png", async () => {
59+
Bootcamp.findById.mockResolvedValue({
60+
_id: "60d5ec49f1b2c80015f8e001",
61+
user: { toString: () => "5d7a514b5d2c12c7449be042" },
62+
});
63+
64+
const fileMoveMock = jest.fn((dest, cb) => cb(null));
65+
req.files = {
66+
file: {
67+
name: "test_image.png",
68+
mimetype: "image/png",
69+
size: 1000,
70+
mv: fileMoveMock,
71+
},
72+
};
73+
74+
process.env.MAX_FILE_UPLOAD = "1000000";
75+
process.env.FILE_UPLOAD_PATH = "./public/uploads";
76+
77+
await bootcampPhotoUpload(req, res, next);
78+
79+
expect(next).not.toHaveBeenCalledWith(expect.any(ErrorResponse));
80+
expect(fileMoveMock).toHaveBeenCalled();
81+
});
82+
});

0 commit comments

Comments
 (0)