Skip to content
العربية

File Upload vulnerabilities

How an upload feature becomes remote code execution, why extension blacklists and magic-byte checks are bypassed, and how to store uploads safely.

Estimated time
25 min
Level
Intermediate
Your progress in this track0%

Before this lesson

An upload feature takes a file from the user and saves it. The danger is simple: if the file is saved somewhere the server will execute, and it contains server code, then uploading it is running code on the server — a web shell, full control. Most upload bugs are a weak check standing between an attacker and that outcome.

What is the vulnerability?

upload.js
fs.writeFileSync('/var/www/uploads/' + req.file.name, req.file.data);

The file is saved under the user’s name, in a folder the web server serves. Upload shell.php containing <?php system($_GET["c"]); ?> and then request /uploads/shell.php?c=... — the server runs your command. The “image upload” became a shell.

Why the usual checks fail

  • No check. Anything goes; the easy case.
  • Extension blacklist. Ban .php and the server still runs .phtml, .php5, .phar, .jsp. A blacklist of executable extensions is always incomplete.
  • Magic bytes + extension suffix. Require the content to start with an image signature and the name to end in .jpg. A A file that is valid as two formats at once — here a real GIF header followed by server code, so it passes an image check and still runs as code. starts with GIF89a and carries code after it, and a double extension like shell.php.jpg passes a suffix check while a misconfigured server still runs it as PHP.

You walk through all three — no check, the .php blacklist, and the magic-byte-plus-double-extension case — across the three labs.

How to prevent it

safe.js
const ALLOW = new Set(['png', 'jpeg']);
if (!isRealImage(data)) return res.status(400).end();   // decode it, do not trust magic alone
const name = randomId() + '.' + typeFor(data);           // our name and extension, not the user's
fs.writeFileSync('/srv/blobs/' + name, data);            // outside the web root, never executed

Rename every upload to a random name with an extension you choose; never keep the user’s name or extension. Store uploads outside the web root (or in object storage) and serve them through a handler that cannot execute code. Validate the type with an allowlist and by actually decoding the content, not by trusting magic bytes or a suffix. Configure the upload folder to execute nothing, and strip double extensions.

Review questionAn upload only rejects files ending in '.php'. Why is that not enough?

Labs

  • Upload with no checks

    EasyBrowser lab

    Upload a server-code file (a web shell) that will run on the server.

  • It bans one extension

    MediumBrowser lab

    Upload a web shell that runs despite the .php ban.

  • It checks the content and the extension

    HardBrowser lab

    Upload a web shell that passes both the content and extension checks and still runs.