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 "image" 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 > 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?
Related Articles