Skip to content

Commit 9dad58d

Browse files
Fix file upload extension validation in bootcamp photo upload
Enforce strict extension whitelist validation on uploaded bootcamp photo files to prevent arbitrary file upload vulnerabilities and Stored XSS via MIME type spoofing. Co-authored-by: SOURAV-ROY <8663561+SOURAV-ROY@users.noreply.github.com>
1 parent 486752e commit 9dad58d

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)