Skip to main content
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 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. 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:
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.
On WordPress, also take uploads only where WordPress takes them:

What you can limit

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

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.

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.

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.