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 throughsession.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.
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.
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
bufferingmiddleware, for every request body on the route; - Caddy as
request_bodywithmax_sizefor uploads.
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 edgelists refusals since each node started, per check.- Prometheus:
dnz_proxy_upload_refusals_total{proxy, node, middleware, check}. - AI assistants: the MCP tools
proxy_upload_refusalsand, with write access,set_upload_limits.