fix(edge): CVE-2026-109618/-109619 multipart mitigations (2026.10.6)

PHP "RCE via OOM corruption" (GHSA-w8hw-w9fx-j79w) and the session
upload-progress variant (GHSA-6wmf-4rv5-p3rp), interim until patched PHP is
on every customer image.

HAProxy (hap_listener.tpl, every frontend render, WAF on or off):
  deny 400 multipart/form-data with no Content-Length (HTTP/1.1 chunked,
  HTTP/2-3 streamed bodies) -- vector (a), the post_max_size bypass.
  Measured: Chrome 155 (h2 and h3) and Firefox 153 send content-length on
  form submits, FormData fetch and XHR; only a ReadableStream fetch body
  (duplex:'half') has none.

Coraza (coraza-spoa/overrides.conf, 209610-209616):
  209610       ARGS_NAMES/FILES_NAMES with more than 16 '['
  209611-613   part-header budget (4/part, deny 32 below zero)
  209614-615   more than 64 empty-filename file parts
  209616       multipart PHP_SESSION_UPLOAD_PROGRESS field
Ids sit in the engine range 200000-209999 that WHP's break-glass rule
(991000001) already removes, so no WHP release is needed for break-glass
to lift them. Each flips the engine On for itself, like the day-one
promotions, so they deny in every mode the sidecar runs in.

Limitation: Coraza sees only the first 16 KB of a body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-21 07:27:42 -07:00
1 parent b6cc3adfaa
commit 729098a8f9
4 files changed
+279 -1

No files matched your search

+28
View File
@@ -616,6 +616,34 @@ frontend web
http-request set-path /suspended if is_suspended_domain
use_backend default-backend if is_suspended_domain
{%- endif %}
# CVE-2026-109618 (GHSA-w8hw-w9fx-j79w, PHP "RCE via OOM corruption"),
# vector (a): a multipart/form-data body with no declared length is not
# bounded by post_max_size -- PHP's multipart parser keeps reading it. A
# body WITH a Content-Length is checked against post_max_size before it is
# parsed. So a multipart request must carry exactly what every browser and
# HTTP library already sends for a form: a Content-Length.
#
# req.hdr_cnt(content-length) is the header as the client sent it, on
# every protocol: HTTP/1.1 chunked has none (and the H1 mux DROPS a
# Content-Length sent alongside Transfer-Encoding, so smuggling one in
# beside chunked does not pass either); HTTP/2 and HTTP/3 have none when
# the client streams the body. H1/H2/H3 all check a declared
# content-length against the DATA received, so it cannot lie about the
# length (same reasoning as the WAF body stamp below).
# Browsers send content-length for form posts and FormData uploads on
# h1/h2/h3; the only body a browser sends without one is a fetch() with a
# ReadableStream body (Chrome, h2/h3 only, `duplex: 'half'`), which is not
# what form uploads use.
#
# Not WAF-gated and not exempt for trusted IPs: this is a protocol
# guarantee the vulnerable parser needs, independent of Coraza and its
# mode. req.fhdr (not req.hdr) so a comma inside the value cannot split it.
# The rest of the advisory (part-header floods, empty file controls, deep
# field names) needs the body and lives in coraza-spoa/overrides.conf
# (209610-209616). Remove once patched PHP is on every customer image.
acl is_multipart_form req.fhdr(content-type) -m sub -i multipart/form-data
http-request deny deny_status 400 hdr x-whp-edge-block "multipart-without-content-length" if is_multipart_form !{ req.hdr_cnt(content-length) gt 0 }
{%- if coraza_spoe_backend %}
# Coraza WAF inspection via SPOE. Runs AFTER rate-limit and IP-block