Application Security

File Upload Security: Block Malicious Uploads

File upload endpoints are the most common way attackers plant web shells. Validate type, size, content and storage location, and uploads stop being your weakest link.

<p>File upload security means controlling what a user can upload so that a supposedly harmless avatar, resume or attachment cannot become a web shell, stored XSS payload or virus distributor. The core rule: never trust the filename, the extension, or the client-declared content type — validate everything server-side and store files where they cannot execute. Done right, uploads become a routine feature; done wrong, they are the single most common initial-access vector we see on compromised sites.</p> <h2>Why Uploads Are the Classic Attack Vector</h2> <p>An uploaded file gives an attacker two things no other input does: persistence and execution. A PHP file renamed to <code>avatar.jpg.php</code>, a <code>.phtml</code> in a writable directory, or an HTML file with script (stored XSS via upload) each hand the attacker long-term control. The filename is fully attacker-controlled, so any validation based on the name alone — including the extension — can be spoofed with tricks like double extensions (<code>shell.php.jpg</code>), null bytes, or case games (<code>.PHP</code>, <code>.PhP</code>).</p> <h2>The Vulnerability Classes to Block</h2> <ul> <li><strong>Remote code execution</strong> — an executable file (php, phtml, jsp, aspx, cgi) stored in a web-served directory</li> <li><strong>Stored XSS</strong> — an HTML/SVG file uploaded and later served inline from your origin executes script in your users' sessions</li> <li><strong>Path traversal</strong> — a filename like <code>../../public/index.php</code> overwrites a live file</li> <li><strong>Denial of service</strong> — a 10 GB &quot;image&quot; or a decompression bomb (a small zip/pxm that expands to gigabytes)</li> <li><strong>Malware hosting</strong> — your domain becomes a trusted-looking distributor of malicious downloads</li> </ul> <h2>Step 1: Validate by Content, Not Just Extension</h2> <p>Read the actual bytes. For images, re-encode them server-side with GD or Imagick — this strips embedded payloads and metadata tricks in one pass:</p> <pre><code class="language-php">$info = getimagesize($tmpPath); if ($info === false) { abort(422, 'Not a real image'); } $img = imagecreatefromstring(file_get_contents($tmpPath)); // re-encode imagejpeg($img, $storagePath, 85); // store only the re-encoded copy </code></pre> <p>For documents, use a dedicated scanner (ClamAV) and a strict allow-list of types you actually accept. Blocklist approaches fail because new dangerous extensions appear faster than lists update.</p> <h2>Step 2: Generate Your Own Filenames</h2> <p>Discard the user's filename entirely. Store files under a server-generated name — <code>post/2026/10/a7f3c9…jpg</code> — and keep the original name only in the database as display metadata. This kills path traversal, double extensions and null-byte tricks in one move, because attacker-controlled bytes never reach the filesystem path.</p> <h2>Step 3: Store Outside the Web Root, Serve Through a Controller</h2> <p>Uploads must never live in a directory the web server executes or serves directly:</p> <ul> <li>Store in <code>storage/app/uploads/</code> (not <code>public/</code>)</li> <li>Serve via a route that reads the file, forces <code>Content-Type</code> from your own mapping, and adds <code>Content-Disposition: attachment</code> or <code>X-Content-Type-Options: nosniff</code></li> <li>For user-facing images, serve from object storage or a separate static domain so an XSS-in-SVG cannot touch your session cookies</li> </ul> <p>If files must be public, disable script execution in that directory (Nginx: <code>location /uploads/ { add_header X-Content-Type-Options nosniff; location ~* \.(php|phtml|phar)$ { deny all; } }</code>).</p> <h2>Step 4: Enforce Size Limits Early and Often</h2> <p>Cap request body size at the server (<code>client_max_body_size 10m;</code>), cap per-file size in the application, and cap total storage per user. For archives, check both compressed and uncompressed sizes before extracting, and refuse ratio bombs (anything above ~100:1 is suspicious).</p> <h2>Step 5: Log, Alert and Age Out</h2> <p>Record every upload (user, hash, size, type verdict) so an incident can be traced. Scan stored files periodically — malware signatures improve after the fact. Delete unclaimed uploads on a schedule; abandoned upload directories are where web shells hide for years.</p> <h2>How 2MNY Tests Upload Endpoints</h2> <p>Our <a href="https://2mny.com/free-security-scan">security scan</a> probes upload handling safely: we check whether executable extensions are accepted, whether uploaded files are served inline from the origin, whether directory execution is disabled, and whether path traversal in filenames is rejected — all with harmless probe files. Full methodology in our <a href="https://2mny.com/kb/evidence-verified-findings">evidence-verified findings article</a>.</p> <h2>Frequently Asked Questions</h2> <h3>Q: Is validating the file extension enough to prevent malicious uploads?</h3> <p>A: No. Extensions are client-controlled metadata — trivially spoofed with double extensions, case variations and null bytes. Validate the actual content (magic bytes, re-encoding), generate your own storage filename, and store outside any executable directory. Extension checks are at most a first filter.</p> <h3>Q: Why is re-encoding images the recommended defense?</h3> <p>A: Re-encoding through GD or Imagick produces a new image containing only pixel data — every embedded payload, EXIF trick or polyglot construction is discarded because it is not pixel data. It also normalizes the format, so a <code>.jpg</code> that was actually a PHP file fails at decode time.</p> <h3>Q: Should uploaded files be served from the main domain?</h3> <p>A: Preferably not. Serve user content from a separate domain or object storage (for example <code>cdn.example.com</code>) with <code>nosniff</code> and no cookies. Then even a successful HTML/SVG upload cannot execute script in your main site's origin — the same-origin policy does the isolation for you.</p> <h3>Q: How do I safely allow zip uploads?</h3> <p>A: Check the uncompressed size and file count before extraction (refuse &gt; 100:1 compression ratios), extract with paths sanitized against traversal, scan the extracted files, and store the result outside the web root. Archive bombs are resource attacks as much as code attacks.</p>

Was this article helpful?

Check if your site has this issue

Our free scan covers this and 40 more checks — no signup to start.

Start Free Scan