Skip to content

File upload security on IIS and Nginx: stop upload-to-RCE

Verified 2 min read

Validate uploads, store them outside the executable root, block script execution in upload paths, and defend against path traversal.

Tested onStandard / Datacenter · Build 26100.33650 · x64
E1
Build
26100.33650
Edition
Standard / Datacenter
Architecture
x64
Support
Documentation (E1)
Evidence
E1Official documentation
Verified
2026-10-29
Page type
Task

An upload is dangerous when three things line up:

  1. the file lands in a path IIS/Nginx serves;
  2. the server executes that extension (.php, .asp, .aspx, .cgi);
  3. the attacker controls the name/content.

Break at least two. Best: store outside the web root and serve through a controlled handler.

  • Allowlist extensions and MIME types; do not trust the Content-Type alone.
  • Enforce a size limit at proxy + app.
  • Generate a random stored name; keep the original only as metadata.
  • Re-encode images if the app can; strip EXIF.
  • Scan with AV/EDR if the business allows.
  • Store uploads outside the site root, or in a directory with no script handlers.
  • Serve downloads with Content-Disposition: attachment and a safe content type.

Reject/normalize:

  • ../, ..\, encoded %2e%2e%2f, double encoding;
  • absolute paths (C:\, \\server\share);
  • NUL bytes and ADS (file.php::$DATA).

Do not build a path from user input and Path.Combine alone; resolve the canonical path and verify it stays under the upload root.

Put uploads in a directory with no managed handler:

<location path="uploads">
<system.webServer>
<handlers>
<clear />
</handlers>
<staticContent>
<clientCache cacheControlMode="NoControl" />
</staticContent>
</system.webServer>
</location>

If you <clear />, the directory serves nothing; usually you want to remove *.php/*.asp handlers, not all static. Test with a real file.

Also use request filtering to deny executable extensions in the upload path.

location ^~ /uploads/ {
add_header Content-Disposition "attachment";
try_files $uri =404;
}
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
}

^~ prevents the regex PHP location from matching files under /uploads/. Without it, shell.php in uploads can be executed. try_files blocks PATH_INFO tricks.

  1. Upload a .php/.aspx file; request it; expect 404/403, not execution.
  2. Upload ..%2fweb.config; expect rejection.
  3. Request a valid image; expect 200 with the right type.
  4. Check the app pool identity cannot write the site root, only uploads.

E1. The lab used PHP FastCGI and IIS but did not run an upload/RCE test; the controls are the production checklist for Server 2025.

Sources