> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dnzwithazee.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload limits

> Decide what multipart uploads to a route may hold, and shield an old PHP from php2shell, before the servers ever see the body.

A route's **Upload limits** decide what `multipart/form-data` uploads to it may hold: how big, how
many fields and files, which fields, and at which paths. A [DNZ Proxy](https://docs.dnzwithazee.com/proxy/dnz-proxy.md) proxy checks
them while the body streams in and refuses what breaks a limit **before your servers see it**. An
app that can't be upgraded (an old PHP, a CMS nobody maintains) is shielded at the proxy, and
ordinary uploads keep working.

Upload limits are off until you turn them on for a route. Requests that aren't uploads aren't
slowed down.

## Shield an old PHP from php2shell

php2shell (CVE-2026-109618 and CVE-2026-109619) is a memory corruption bug in PHP's upload handling,
reached through `session.upload_progress` with crafted uploads. The real fix is a PHP upgrade. For an
app that is stuck on an old PHP, turn on the **php2shell shield**:

**1. Open the route**

On the proxy's page, open the route in front of the PHP app (**Routes** → the route).

**2. Turn on the shield**

Under **Shields for apps that can't be upgraded**, tick **php2shell**, next to the
[other shields](https://docs.dnzwithazee.com/proxy/shields.md). Then switch on **Upload limits** if you also want a
**Largest upload**, and set it to the largest file your app takes.

**3. Save**

Every node has it within a second.

Or from the terminal:

```bash theme={null}
dnz proxy routes edit edge shop.example.com --php2shell --upload-max 20M
```

The shield refuses every upload holding PHP's upload progress field (`PHP_SESSION_UPLOAD_PROGRESS`,
or the name your `session.upload_progress.name` is set to), and keeps the limits you leave empty
tight: at most 100 fields and files, form fields up to 64 KiB, short part headers. The field never
reaches PHP: the proxy holds each part's headers until they are checked, and answers the visitor
with a `400` page instead.

> [!TIP]
>
> On WordPress, also take uploads only where WordPress takes them:
>
> ```bash theme={null}
> dnz proxy routes edit edge blog.example.com --php2shell --upload-max 64M \
>   --upload-path /wp-admin/async-upload.php --upload-path /wp-admin/media-new.php --upload-method POST
> ```

## What you can limit

| Setting | Visitors get |
| - | - |
| **Take no uploads at all** | `415` |
| **Only at these paths** (and below them), **Methods** | `403` elsewhere |
| **Largest upload** | `413` |
| **At most … fields and files** | `413` |
| **Largest file**, **Largest field** | `413` |
| **Refuse uploads with these fields** (any case) | `400` |

Paths are prefixes: `/upload` takes `/upload` and `/upload/photo`, not `/uploads`. Paths that try
to climb out (`/upload/../admin.php`, encoded slashes) never count as a listed path.

The proxy also refuses, with `400`, uploads that a server could read differently from the proxy:
malformed multipart, a boundary twice, a field named twice, header lines PHP would split or join,
and the like. Browsers never send these, so real uploads aren't affected.

Under **Proxy → Middlewares** (with **Routes' own** on) the route's `dnzMultipart` middleware has every limit, including
part headers, file name length and boundary length.

## How the upload reaches your servers

Your servers get the upload unchanged, as it streams in: each part once its headers are checked,
each form field once it is whole, and file data straight away. Nothing is buffered, so large uploads
stream through. When an upload breaks a limit halfway, the proxy cuts the request to your servers off
and shows the visitor an **Upload refused** page saying what was too big or not allowed (HTML, JSON or
text, like the [status pages](https://docs.dnzwithazee.com/proxy/status-pages.md)).

## What it costs

Routes without upload limits don't pay anything. With them, on one CPU of a DNZ Proxy node, a small
form takes 9 µs more per request at the same rate, a 1 MiB file 14% more CPU at the same rate, and a
16 MiB file, where the CPU is full, 6% fewer uploads a second. See [Proxy benchmarks](https://docs.dnzwithazee.com/proxy/benchmarks.md).

## Every route at once

On a DNZ Proxy, **Settings → Every route → Upload limits** applies to every route that has no
upload limits of its own.

```bash theme={null}
dnz proxy defaults edge --uploads --upload-max 50M
```

## Traefik and Caddy

Traefik and Caddy proxies get **Largest upload** alone:

* Traefik as its `buffering` middleware, for every request body on the route;
* Caddy as `request_body` with `max_size` for uploads.

The php2shell shield, fields, paths and the other checks need a DNZ Proxy.

## See what was refused

* The request log shows each refused upload with `block upload <check>`, and the analytics count it
  as blocked.
* `dnz proxy routes refusals edge` lists refusals since each node started, per check.
* Prometheus: `dnz_proxy_upload_refusals_total{proxy, node, middleware, check}`.
* AI assistants: the MCP tools `proxy_upload_refusals` and, with write access, `set_upload_limits`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.