Defend the Web (Week 6): File Upload Defense

How to Secure File Uploads, Block Web Shells & Prevent Remote Code Execution Attacks

In partnership with

πŸ›‘οΈ Week 6: File Upload Defense

Defend the Web: Part 6 of 8

Welcome back to Defensive Wednesday.

Hey πŸ‘‹

Yesterday you weaponized file uploads. You disguised web shells as profile pictures, bypassed filters with null bytes and polyglot files, and took over servers. You turned "upload your resume" into remote code execution. If you missed it, catch up here πŸ‘‰ Week 6

Today? We lock it down.

You're the developer now. Your job is to make sure every file that lands on your server is safe β€” even when attackers throw shell.php.jpg and magic byte tricks at every upload form.

Here's what most devs get wrong β€” they think checking file extensions is enough. It's not even close.

File uploads are the front door to your server. The Equifax breach started here. Ransomware campaigns use it as an entry point. Bug bounty hunters find these weekly and collect $10K+ payouts.

The fix? Actually hard. Way harder than "just check the extension."

You need to validate everything. Store files outside the web root. Rename them. Scan them. Treat every upload as hostile.

Let's build upload systems that don't get pwned.

πŸ’°What if Business news was actually... fun to read?
Click here πŸ‘‰

Business news worth its weight in gold

You know what’s rarer than gold? Business news that’s actually enjoyable.

That’s what Morning Brew delivers every day β€” stories as valuable as your time. Each edition breaks down the most relevant business, finance, and world headlines into sharp, engaging insights you’ll actually understand β€” and feel confident talking about.

It’s quick. It’s witty. And unlike most news, it’ll never bore you to tears. Start your mornings smarter and join over 4 million people reading Morning Brew for free.

🎯 The Core Problem

Every file upload RCE happens because your server executes user-uploaded files as code.

When users upload files and you serve those files directly from a web-accessible directory, you're asking for trouble.

They upload shell.php. Your server stores it in /uploads/. They visit yoursite.com/uploads/shell.php. Your server executes it. Game over.

Three golden rules:

  1. Never store uploads in the web root β€” Files should never be directly accessible via URL

  2. Validate everything β€” Extension, MIME type, content, size, magic bytes

  3. Rename and relocate β€” Change filename, strip path info, store outside web directory

Follow these, and file upload RCE becomes nearly impossible.

πŸ”’ Defense #1: Store Uploads Outside Web Root

This is your number one defense. Not optional.

The problem: If uploads are in /var/www/html/uploads/, attackers access them at yoursite.com/uploads/shell.php.

The solution: Store files completely outside your web directory.

Where to store files:

βœ… /var/uploads/ (outside /var/www/)
βœ… /opt/app-uploads/
βœ… Cloud storage (S3, Azure Blob, Google Cloud Storage)
βœ… Separate file server

How to serve them safely:

Use a download script that validates permissions and serves files programmatically.

Bad: User visits yoursite.com/uploads/file.php β†’ Server executes

Good: User requests yoursite.com/download?id=abc123 β†’ Script validates β†’ Reads from /var/uploads/abc123.bin β†’ Serves with proper headers

Your download script checks permissions, file type, and forces download instead of execution.

Why this matters: Even if attackers upload a perfect web shell, the file lives outside the web root. The server never executes it.

πŸ”’ Defense #2: Whitelist File Extensions

Extension validation is your first line. But most devs do it wrong.

Bad (blacklist): Block .php, .exe, .sh

Attackers bypass with .php5, .phtml, .phar, .PhP.

Good (whitelist): Allow ONLY .jpg, .jpeg, .png, .gif, .pdf. Everything else gets rejected.

Implementation:

βœ… Strict whitelist of allowed extensions
βœ… Check extension after the last dot
βœ… Convert to lowercase before checking
βœ… Strip null bytes from filename
βœ… Remove path traversal attempts (../, ..)
βœ… Block double extensions

Why this matters: Whitelisting blocks every bypass. Null bytes? Stripped. Double extensions? Rejected. Case variations? Normalized.

πŸ”’ Defense #3: Validate MIME Types & Magic Bytes

MIME type validation catches some attacks, but attackers can spoof headers. Use server-side detection.

Server-side MIME detection:

Use libraries that read actual file content, not just the Content-Type header.

Expected types:
Images: image/jpeg, image/png, image/gif
PDFs: application/pdf

Magic bytes (file signatures):

JPEG: FF D8 FF
PNG: 89 50 4E 47
GIF: 47 49 46 38
PDF: 25 50 44 46

Read the first bytes of uploaded files. Compare against expected signatures. Reject mismatches.

Resources:
πŸ“š python-magic
πŸ“š file-type (Node.js)
πŸ“š PHP fileinfo
πŸ“š File Signature Database

Why this matters: Attackers send shell.php with Content-Type: image/jpeg. Server-side magic number detection catches it.

πŸ”’ Defense #4: Rename Files & Strip Metadata

Never trust user-provided filenames.

Renaming strategy:

βœ… Generate random filename (UUID, hash, timestamp)
βœ… Strip all path information
βœ… Store mapping in database (original β†’ safe name)
βœ… Never use user input in file paths

Example:
Original: ../../etc/passwd.jpg
Stored as: a3f5e892-4c3b-11ed-bdc3-0242ac120002.jpg

For images, re-encode them to strip embedded code and metadata. This destroys polyglot files.

Image processing libraries:
πŸ“š Sharp (Node.js)
πŸ“š Imagick (PHP)
πŸ“š exiftool β€” Metadata stripping

Why this matters: Path traversal attempts become meaningless. Polyglot files don't survive re-encoding.

πŸ”’ Defense #5: Disable Script Execution in Upload Directories

Even if a shell lands in your uploads folder, prevent execution.

Apache (.htaccess in upload directory):

<FilesMatch "\.ph(p[3457]?|t|tml)$">
    Deny from all
</FilesMatch>

nginx (in server block):

location /uploads/ {
    location ~ \.php$ {
        deny all;
    }
}

Why this matters: Defense in depth. Even if shell.php gets through, the web server refuses to execute it.

πŸ”’ Defense #6: Scan with Antivirus & Set File Limits

Run every uploaded file through scanning before storing.

Scanning workflow:

Upload β†’ Temporary quarantine β†’ Run scan β†’ If clean: move to storage β†’ If infected: delete and alert

Scanning solutions:
πŸ“š VirusTotal API β€” Multi-engine scanning
πŸ“š YARA Rules β€” Malware pattern matching

File size limits:

Set maximum sizes to prevent DoS attacks.

Recommended limits:
Profile pictures: 2-5 MB
Documents: 10-20 MB
General uploads: 5 MB default

Configure at web server level (nginx client_max_body_size, Apache LimitRequestBody) and application level.

Resources:
πŸ“š nginx Upload Limits
πŸ“š Apache Upload Limits

Why this matters: Known malware gets caught. Automated scanners can't flood your storage with massive files.

πŸ”’ Defense #7: Use Secure Headers & Rate Limiting

When serving files, force downloads and prevent execution.

Headers to set:

Content-Disposition: attachment; filename="user-file.pdf"
Content-Type: application/octet-stream
X-Content-Type-Options: nosniff

Content-Disposition: attachment forces download, prevents rendering.
X-Content-Type-Options: nosniff prevents MIME-sniffing attacks.

Rate limiting:

βœ… Limit uploads per user per time period
βœ… Track upload attempts by IP
βœ… Implement CAPTCHA for suspicious activity

Example limits:
10 uploads per hour per user
3 failed uploads trigger CAPTCHA
20 uploads per day for new accounts

Why this matters: XSS in uploaded HTML fails. Automated attacks can't brute-force bypasses.

πŸ”’ Defense #8: Cloud Storage & Logging

If using cloud storage, configure it securely.

Cloud storage security:

βœ… Never allow public write access
βœ… Use presigned URLs for uploads
βœ… Set bucket policies correctly
βœ… Enable versioning and lifecycle policies
βœ… Use separate buckets for uploads vs. public content

Comprehensive logging:

Log every upload attempt, validation failure, scanner result, and file access. Monitor for unusual patterns.

What to monitor:
🚨 Unusual file extensions
🚨 Large numbers of failed uploads
🚨 Repeated validation failures
🚨 Scanner detections

Resources:
πŸ“š OWASP Logging Cheat Sheet
πŸ“š ELK Stack

Why this matters: Misconfigured S3 buckets are a top vulnerability. Logging helps catch attacks in progress.

πŸ› οΈ Essential Defense Tools

Validation:
πŸ“š python-magic β€” MIME detection
πŸ“š file-type β€” Node.js type detection
πŸ“š Apache Tika β€” Java content detection

Scanning:
πŸ“š YARA β€” Pattern matching
πŸ“š VirusTotal β€” Multi-engine scanner

Image Processing:
πŸ“š Sharp β€” Node.js images

Testing:
πŸ“š Burp Suite β€” Upload testing
πŸ“š Upload Scanner β€” Burp extension
πŸ“š Fuxploider β€” Automated scanner

πŸ“š Learning Resources

πŸ“š OWASP File Upload Cheat Sheet β€” Complete reference
πŸ“š PortSwigger File Upload Guide β€” Attack and defense
πŸ“š CWE-434: Unrestricted Upload β€” Official weakness description

That's Week 6 defense done. πŸ›‘οΈ

Next Tuesday: SSRF & Internal Access β€” we're making servers attack themselves and accessing internal networks.

Next Wednesday: SSRF Defense β€” how to prevent server-side request forgery and protect internal resources.

See you then.

β€” Zwire ✌️

Your Feedback Matters

Did You Enjoy This Week’s Defensive Tutorial?

Login or Subscribe to participate in polls.

P.S. Got questions about implementing these defenses? Reply to this email. I read everything.

Reply

or to participate.