# Secure File Uploads: What Most Developers Get Wrong

File upload features feel simple to build. A user picks a file, your server saves it, done. But upload endpoints are one of the most common ways attackers get a foothold in a web app.

Here's how to lock yours down, using a typical Node.js/Express stack as the example.

## TL;DR

*   Validate files by content signature, not extension or MIME header
    
*   Cap file size at the middleware layer
    
*   Rename files server-side, never reuse client input
    
*   Store outside the web root or in access-controlled object storage
    
*   Scan for malware before files are usable
    
*   Re-encode images rather than storing the raw upload
    

* * *

## 1\. Never Trust the File Extension or MIME Type

A file named `photo.jpg` can still be a PHP script or executable. Browsers send the `Content-Type` header based on the extension, and attackers can set that header to whatever they want.

Instead, check the file's actual content (its "magic number" or byte signature) rather than trusting metadata.

```javascript
import { fileTypeFromBuffer } from 'file-type';

const buffer = await file.arrayBuffer();
const type = await fileTypeFromBuffer(Buffer.from(buffer));

if (!type || !['image/jpeg', 'image/png'].includes(type.mime)) {
  return res.status(400).json({ error: 'Invalid file type' });
}
```

## 2\. Enforce Strict Size Limits

Unbounded uploads are a denial-of-service vector; a single request can fill your disk or memory. Set limits at the middleware level, not just in your frontend form.

```javascript
import multer from 'multer';

const upload = multer({
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
});
```

## 3\. Generate Your Own Filenames

Never use the client-supplied filename as-is. It can contain path traversal sequences (`../../etc/passwd`) or characters that break your filesystem or storage bucket.

```javascript
import { randomUUID } from 'crypto';
import path from 'path';

const safeName = `${randomUUID()}${path.extname(originalName)}`;
```

## 4\. Store Uploads Outside the Web Root (or in Object Storage)

If uploaded files live in a publicly served directory, an attacker who sneaks in an executable script could get it run directly. Store files in a location the server doesn't execute, or better, in object storage like S3, with public access disabled by default and signed URLs for retrieval.

> **Tip:** Treat every uploaded file as untrusted content until proven otherwise, the same way you'd treat user input in a form field.

## 5\. Scan for Malware Before Files Touch Production

Even with type-checking, a valid image can carry embedded exploits (e.g. malformed EXIF data). Running uploads through a scanner like ClamAV, or a managed service such as AWS's file scanning integrations, adds a real safety net.

```javascript
import { exec } from 'child_process';

exec(`clamscan ${filePath}`, (err, stdout) => {
  if (stdout.includes('FOUND')) {
    // quarantine or delete the file
  }
});
```

## 6\. Re-encode Images Instead of Trusting Them As-Is

For image uploads specifically, running the file through a library like `sharp` to resize or re-encode it strips out most embedded payloads, since you're regenerating the pixel data rather than passing through the original bytes.

```javascript
import sharp from 'sharp';

await sharp(inputBuffer)
  .resize(1200, 1200, { fit: 'inside' })
  .toFile(outputPath);
```

## Wrapping Up

None of this is exotic. It's a handful of small habits that close off one of the most exploited entry points in web apps, and they take less time to implement than the incident response that follows skipping them.

* * *

*Found this useful? Drop a comment with how you handle uploads in your stack, I'd love to compare notes.*
