Skip to content

Commit fdb398f

Browse files
Merge pull request #34 from kucc1997/fix-upload-mime-validation
fix: validate uploaded file MIME signatures
2 parents 5a80446 + c0ebb0f commit fdb398f

File tree

4 files changed

+174
-74
lines changed

4 files changed

+174
-74
lines changed

src/app/api/papers/post.ts

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,101 @@ import { customAlphabet } from "nanoid";
55
import path from "path";
66
import { writeFile, mkdir } from "fs/promises";
77
import { getUploadDir, getUploadPublicBasePath } from "@/lib/upload-path";
8+
import { detectMimeType } from "@/lib/file-upload-validation";
89

910
const UPLOAD_DIR = getUploadDir();
1011
const UPLOAD_PUBLIC_BASE = getUploadPublicBasePath();
1112

1213
export default async function POST(req: Request) {
13-
const formData = await req.formData();
14-
const data = parseFormData(formData)
15-
16-
const paperID = await generateSubmissionId();
14+
try {
15+
const formData = await req.formData();
16+
const data = parseFormData(formData);
17+
const fileBuffer = Buffer.from(await data.file.arrayBuffer());
18+
const detectedMimeType = detectMimeType(fileBuffer);
19+
20+
if (detectedMimeType !== "application/pdf") {
21+
return Response.json(
22+
{
23+
success: false,
24+
data: "Invalid file type. Please upload a valid PDF file.",
25+
},
26+
{
27+
status: 400,
28+
}
29+
);
30+
}
1731

18-
// Upload file
19-
const uploadDir = path.join(process.cwd(), UPLOAD_DIR, "papers");
20-
const filePath = path.join(uploadDir, `${paperID}.pdf`);
32+
const paperID = await generateSubmissionId();
33+
34+
// Upload file
35+
const uploadDir = path.join(process.cwd(), UPLOAD_DIR, "papers");
36+
const filePath = path.join(uploadDir, `${paperID}.pdf`);
37+
38+
// Ensure the upload directory exists
39+
await mkdir(uploadDir, { recursive: true });
40+
await writeFile(filePath, fileBuffer);
41+
42+
const authorFromDb = await db.select({ id: users.id }).from(users).where(
43+
eq(users.email, data.coAuthors?.[0]?.email)
44+
);
45+
if (authorFromDb.length === 0) {
46+
return Response.json(
47+
{
48+
success: false,
49+
data: "First author not registered! Please make sure the email matches with your GitHub's primary email.",
50+
},
51+
{
52+
status: 400,
53+
}
54+
);
55+
}
2156

22-
// Ensure the upload directory exists
23-
await mkdir(uploadDir, { recursive: true });
24-
await writeFile(filePath, Buffer.from(await data.file.arrayBuffer()));
57+
const paperReturned = await db
58+
.insert(papers)
59+
.values({
60+
title: data.title,
61+
abstract: data.abstract,
62+
keywords: data.keywords,
63+
fileUrl: `${UPLOAD_PUBLIC_BASE}/papers/${paperID}.pdf`,
64+
themeId: data.theme,
65+
trackType: data.trackType,
66+
authorId: authorFromDb[0].id,
67+
submissionId: paperID,
68+
})
69+
.returning({ id: papers.id, submissionId: papers.submissionId });
70+
71+
await Promise.all(
72+
data.coAuthors.map(async (coauthor, index) => {
73+
if (index >= 1) {
74+
await db.insert(coAuthors).values({
75+
name: coauthor.name,
76+
email: coauthor.email,
77+
paperId: paperReturned[0].id,
78+
orcid: coauthor?.orcid,
79+
affiliation: coauthor?.affiliation,
80+
});
81+
}
82+
})
83+
);
2584

26-
const authorFromDb = await db.select({ id: users.id }).from(users).where(
27-
eq(users.email, data.coAuthors?.[0]?.email)
28-
);
29-
if (authorFromDb.length === 0) {
3085
return Response.json({
31-
success: false,
32-
data: "First author not registered! Please make sure the email matches with your GitHub's primary email."
33-
}, {
34-
status: 400
35-
})
36-
}
37-
38-
const paperReturned = await db
39-
.insert(papers)
40-
.values({
41-
title: data.title,
42-
abstract: data.abstract,
43-
keywords: data.keywords,
44-
fileUrl: `${UPLOAD_PUBLIC_BASE}/papers/${paperID}.pdf`,
45-
themeId: data.theme,
46-
trackType: data.trackType,
47-
authorId: authorFromDb[0].id,
48-
submissionId: paperID
49-
}).returning({ id: papers.id, submissionId: papers.submissionId })
50-
51-
await Promise.all(
52-
data.coAuthors.map(async (coauthor, index) => {
53-
if (index >= 1) { // Neglecting the first co-author
54-
await db.insert(coAuthors).values({
55-
name: coauthor.name,
56-
email: coauthor.email,
57-
paperId: paperReturned[0].id,
58-
orcid: coauthor?.orcid,
59-
affiliation: coauthor?.affiliation
60-
})
86+
success: true,
87+
data: {
88+
submissionId: paperReturned[0].submissionId,
89+
},
90+
});
91+
} catch (error) {
92+
console.error("Paper submission error:", error);
93+
return Response.json(
94+
{
95+
success: false,
96+
data: "Failed to process paper submission.",
97+
},
98+
{
99+
status: 500,
61100
}
62-
})
63-
)
64-
65-
return Response.json({
66-
success: true, data: {
67-
submissionId: paperReturned[0].submissionId
68-
}
69-
})
101+
);
102+
}
70103
}
71104

72105
async function generateSubmissionId() {

src/app/api/registration/route.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import path from "path"
66
import { writeFile, mkdir } from "fs/promises"
77
import { sendRegistrationEmail } from "@/lib/mail"
88
import { getUploadDir, getUploadPublicBasePath } from "@/lib/upload-path"
9+
import { detectMimeType, getExtensionForMimeType } from "@/lib/file-upload-validation"
910

1011
const UPLOAD_DIR = getUploadDir()
1112
const UPLOAD_PUBLIC_BASE = getUploadPublicBasePath()
@@ -61,13 +62,16 @@ export async function POST(req: Request) {
6162
// Upload payment voucher
6263
const uploadDir = path.join(process.cwd(), UPLOAD_DIR, "vouchers")
6364

64-
// Get file extension from the uploaded file
65-
const fileExtension = paymentVoucher.name.split('.').pop()?.toLowerCase()
66-
if (!fileExtension || !['pdf', 'png', 'jpg', 'jpeg'].includes(fileExtension)) {
65+
const voucherBuffer = Buffer.from(await paymentVoucher.arrayBuffer())
66+
const detectedMimeType = detectMimeType(voucherBuffer)
67+
const fileExtension = detectedMimeType ? getExtensionForMimeType(detectedMimeType) : null
68+
const allowedMimeTypes = new Set(["application/pdf", "image/png", "image/jpeg"])
69+
70+
if (!detectedMimeType || !allowedMimeTypes.has(detectedMimeType) || !fileExtension) {
6771
return NextResponse.json(
6872
{
6973
success: false,
70-
data: "Invalid file type. Please upload PDF, PNG, or JPG files only."
74+
data: "Invalid file type. Please upload a valid PDF, PNG, or JPG file."
7175
},
7276
{ status: 400 }
7377
)
@@ -77,7 +81,7 @@ export async function POST(req: Request) {
7781

7882
// Ensure the upload directory exists
7983
await mkdir(uploadDir, { recursive: true })
80-
await writeFile(filePath, Buffer.from(await paymentVoucher.arrayBuffer()))
84+
await writeFile(filePath, voucherBuffer)
8185

8286
// Insert registration into database
8387
const registration = await db.insert(registrations).values({

src/app/api/upload/route.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { NextRequest, NextResponse } from "next/server";
33
import path from "path";
44
import { auth } from "@/auth";
55
import { getUploadDir, getUploadPublicBasePath } from "@/lib/upload-path";
6+
import {
7+
detectMimeType,
8+
getExtensionForMimeType,
9+
sanitizeFileStem,
10+
} from "@/lib/file-upload-validation";
611

712
const UPLOAD_DIR = getUploadDir();
813
const UPLOAD_PUBLIC_BASE = getUploadPublicBasePath();
@@ -21,34 +26,37 @@ export async function POST(request: NextRequest) {
2126
return NextResponse.json({ error: "No file provided" }, { status: 400 });
2227
}
2328

24-
// Validate file type
25-
const allowedTypes = [
29+
// Validate file size (10MB max)
30+
const maxSize = 10 * 1024 * 1024; // 10MB
31+
if (file.size > maxSize) {
32+
return NextResponse.json(
33+
{ error: "File size exceeds 10MB limit" },
34+
{ status: 400 }
35+
);
36+
}
37+
38+
const buffer = Buffer.from(await file.arrayBuffer());
39+
const detectedMimeType = detectMimeType(buffer);
40+
const fileExtension = detectedMimeType
41+
? getExtensionForMimeType(detectedMimeType)
42+
: null;
43+
const allowedMimeTypes = new Set([
2644
"application/pdf",
2745
"image/jpeg",
28-
"image/jpg",
2946
"image/png",
3047
"image/webp",
3148
"image/gif",
32-
];
49+
]);
3350

34-
if (!allowedTypes.includes(file.type)) {
51+
if (!detectedMimeType || !allowedMimeTypes.has(detectedMimeType) || !fileExtension) {
3552
return NextResponse.json(
36-
{ error: "Invalid file type. Only PDF and images are allowed." },
53+
{ error: "Invalid file type. Only PDF and supported image files are allowed." },
3754
{ status: 400 }
3855
);
3956
}
4057

41-
// Validate file size (10MB max)
42-
const maxSize = 10 * 1024 * 1024; // 10MB
43-
if (file.size > maxSize) {
44-
return NextResponse.json(
45-
{ error: "File size exceeds 10MB limit" },
46-
{ status: 400 }
47-
);
48-
}
49-
50-
const buffer = Buffer.from(await file.arrayBuffer());
51-
const filename = `${Date.now()}-${file.name.replace(/\s+/g, "-")}`;
58+
const stem = sanitizeFileStem(file.name);
59+
const filename = `${Date.now()}-${stem}.${fileExtension}`;
5260
const uploadDir = path.join(process.cwd(), UPLOAD_DIR, "archive");
5361
const filepath = path.join(uploadDir, filename);
5462

src/lib/file-upload-validation.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const MIME_EXTENSION_MAP: Record<string, string> = {
2+
"application/pdf": "pdf",
3+
"image/jpeg": "jpg",
4+
"image/png": "png",
5+
"image/gif": "gif",
6+
"image/webp": "webp",
7+
};
8+
9+
function hasPrefix(buffer: Buffer, bytes: number[]) {
10+
return bytes.every((byte, index) => buffer[index] === byte);
11+
}
12+
13+
export function detectMimeType(buffer: Buffer): string | null {
14+
if (buffer.length >= 4 && hasPrefix(buffer, [0x25, 0x50, 0x44, 0x46])) {
15+
return "application/pdf";
16+
}
17+
18+
if (
19+
buffer.length >= 8 &&
20+
hasPrefix(buffer, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
21+
) {
22+
return "image/png";
23+
}
24+
25+
if (buffer.length >= 3 && hasPrefix(buffer, [0xff, 0xd8, 0xff])) {
26+
return "image/jpeg";
27+
}
28+
29+
if (buffer.length >= 4 && hasPrefix(buffer, [0x47, 0x49, 0x46, 0x38])) {
30+
return "image/gif";
31+
}
32+
33+
if (
34+
buffer.length >= 12 &&
35+
buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
36+
buffer.subarray(8, 12).toString("ascii") === "WEBP"
37+
) {
38+
return "image/webp";
39+
}
40+
41+
return null;
42+
}
43+
44+
export function getExtensionForMimeType(mimeType: string) {
45+
return MIME_EXTENSION_MAP[mimeType] || null;
46+
}
47+
48+
export function sanitizeFileStem(fileName: string) {
49+
return fileName
50+
.replace(/[\\/]/g, "-")
51+
.replace(/\.[^.]+$/, "")
52+
.replace(/[^a-zA-Z0-9._-]/g, "-")
53+
.replace(/-+/g, "-")
54+
.replace(/^-|-$/g, "") || "file";
55+
}

0 commit comments

Comments
 (0)