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

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

**Tested on**

- Build: 26100.33650
- Edition: Standard / Datacenter
- Architecture: x64
- Evidence: E1
- Verified: 2026-10-29

## The short answer

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.

## Validation checklist

- 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.

## Path traversal

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.

## IIS

Put uploads in a directory with no managed handler:

```xml
<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.

## Nginx

```nginx
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.

> **Danger: Upload + executable directory = RCE**
>
> If an uploaded `.php`/`.aspx` file is served and executed, the attacker has code execution as the app pool/service identity. This is not “just a file upload bug”.

## Verify

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.

## Lab boundary

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

- [OWASP — File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html)
- [OWASP — Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal)
- [Microsoft Learn — Request filtering](https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/)

---

Source: https://windows.homes/en/server/security/upload/
