Skip to content

fix: add missing upload mime type validation#7

Open
sharathlingam wants to merge 1 commit into
FOSSUChennai:mainfrom
sharathlingam:fix/security-bug
Open

fix: add missing upload mime type validation#7
sharathlingam wants to merge 1 commit into
FOSSUChennai:mainfrom
sharathlingam:fix/security-bug

Conversation

@sharathlingam

Copy link
Copy Markdown

Fixes: #3

@sharathlingam

Copy link
Copy Markdown
Author

@JustinBenito can you please review it.

@nammahari

Copy link
Copy Markdown
Collaborator

Hi , I can see a lot changes , can add what was the existing backend , what you have changed and why do you think that would better ?

@sharathlingam

Copy link
Copy Markdown
Author

What Was the Existing Backend?

Backend API (app/api/upload/route.ts)

The original implementation:

  • Accepted: JSON payload with contentType and size (metadata only, not the actual file)
  • Validation: Performed validation checks directly in the route handler
  • Constants: Validation rules (ALLOWED_TYPES, MAX_FILE_SIZE) were defined locally in the route file
  • Flow: Client → Send JSON metadata → Server validates → Generate presigned URL → Client uploads file
// Old approach
const body = await request.json();
const { contentType, size } = body;

if (!ALLOWED_TYPES.includes(contentType)) {
  return NextResponse.json({ error: "Invalid file type..." }, { status: 400 });
}

if (size > MAX_FILE_SIZE) {
  return NextResponse.json({ error: "File too large..." }, { status: 400 });
}

Frontend (app/page.tsx)

  • No client-side validation: Files were sent to the API without pre-validation
  • JSON payload: Sent only metadata (contentType and size) to the backend
  • User experience: Users had to wait for a server round-trip to discover validation errors

What Has Changed?

1. New Centralized Constants (lib/constants.ts)

Created a new file to centralize validation constants:

export const ALLOWED_TYPES = [
  "image/jpeg",
  "image/jpg",
  "image/png",
  "image/gif",
  "image/webp",
];
export const MAX_FILE_SIZE = 10 * 1024 * 1024;

Why: Single source of truth for validation rules, making it easier to maintain and update.

2. Reusable Validation Function (lib/utils.ts)

Added a new validateImageFile() function:

export function validateImageFile(file: File): ImageValidationResult {
  // Validates file type and size
  // Returns { valid: boolean, error?: string }
}

Why:

  • DRY (Don't Repeat Yourself) principle
  • Can be used both client-side and server-side
  • Consistent validation logic across the application
  • Type-safe with TypeScript interfaces

3. Backend API Changes (app/api/upload/route.ts)

  • Changed from: Accepting JSON with metadata
  • Changed to: Accepting FormData with the actual File object
  • Validation: Now uses the centralized validateImageFile() function
  • Removed: Local constant definitions (now imported from lib/constants.ts)
// New approach
const imageFile = (await request.formData()).get("image") as File;
const { valid, error } = validateImageFile(imageFile);

if (!valid) {
  throw new Error(error || "Error Uploading the file, Please try again");
}

4. Frontend Changes (app/page.tsx)

  • Added: Client-side validation before making API call
  • Changed: Sends FormData with actual file instead of JSON metadata
  • Improved UX: Users get immediate feedback on validation errors without waiting for server response
// Client-side validation first
const imageValidatedResponse = validateImageFile(file);
if (!imageValidatedResponse.valid) {
  throw new Error(
    imageValidatedResponse.error || "Error Uploading the file..."
  );
}

// Then send FormData with actual file
const formData = new FormData();
formData.append("image", file);

Why Are These Changes Better?

1. Better User Experience

  • Immediate feedback: Validation errors are caught client-side before any network request
  • Faster error detection: Users don't have to wait for server round-trip to know their file is invalid
  • Reduced server load: Invalid files are rejected before reaching the API

2. Code Maintainability 🔧

  • Single source of truth: Validation rules are centralized in lib/constants.ts
  • Reusable validation: validateImageFile() can be used anywhere in the codebase
  • Easier updates: Changing validation rules only requires updating one file
  • Consistent validation: Same logic used on both client and server

3. Better Security 🔒

  • Server-side validation: Backend still validates the actual file, not just metadata
  • Type safety: TypeScript interfaces ensure type-safe validation results
  • Actual file validation: Server receives and validates the real file, preventing metadata spoofing

4. Improved Architecture 🏗️

  • Separation of concerns: Validation logic is separated from route handlers
  • Testability: Validation function can be easily unit tested in isolation
  • Scalability: Easy to extend validation rules (e.g., add image dimension checks)

@nammahari drafted this with AI. This PR fixes #3

@HarshPatel5940 HarshPatel5940 changed the title feat: implement image upload validation using mime type fix: add missing upload mime type validation Jan 12, 2026
@nammahari

Copy link
Copy Markdown
Collaborator

@sharathlingam, the project is running on a free tier in Vercel. I doubt that adding strict validation and restricting the upload will eat compute.

Do you have any other approach to solve the issue?

@sharathlingam

sharathlingam commented Jan 13, 2026

Copy link
Copy Markdown
Author

@sharathlingam, the project is running on a free tier in Vercel. I doubt that adding strict validation and restricting the upload will eat compute.

Do you have any other approach to solve the issue?

What about we validate the mime type only in the client side and revert the server side validation to the previous state, the one which validates only the size and the file type?

@nammahari

Comment thread app/api/upload/route.ts
Comment on lines -5 to -29
const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
const MAX_FILE_SIZE = 10 * 1024 * 1024;
import { nanoid } from "nanoid";
import { type NextRequest, NextResponse } from "next/server";
import { generatePresignedUpload } from "@/lib/r2-client";
import { validateImageFile } from "@/lib/utils";

export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { contentType, size } = body;

if (!contentType) {
return NextResponse.json({ error: 'Content type is required' }, { status: 400 });
}

if (!ALLOWED_TYPES.includes(contentType)) {
return NextResponse.json(
{ error: 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.' },
{ status: 400 }
);
}

if (size > MAX_FILE_SIZE) {
return NextResponse.json(
{ error: 'File too large. Maximum size is 10MB.' },
{ status: 400 }
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt this validation already?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security Bug

3 participants