https://bz.apache.org/bugzilla/show_bug.cgi?id=70122
Bug ID: 70122
Summary: HTTP response splitting via unsanitized 1xx interim
status line in mod_proxy_http /
ap_send_interim_response
Product: Apache httpd-2
Version: 2.4.68
Hardware: PC
OS: Linux
Status: NEW
Severity: normal
Priority: P2
Component: mod_proxy_http
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: ---
Hello Apache HTTP Server Security Team!
I believe I have found an incomplete fix for CVE-2026-33523 ("HTTP response
splitting forwarding malicious status line"). The same class of issue is still
reachable through the interim (1xx) response path, which the fix does not
cover.
I am reporting this privately and have not disclosed it anywhere.
== Summary ==
ap_send_interim_response() (server/protocol.c) writes r->status_line directly
to
the client without the sanitization that validate_status_line() performs on the
normal (final) response path (modules/http/http_filters.c). When mod_proxy_http
forwards a 1xx interim response from an HTTP/1.x backend, r->status_line is
taken
verbatim from the origin's status line (modules/proxy/mod_proxy_http.c). Its
reason phrase may contain control characters -- in particular a bare CR (0x0D),
and any C0 control except LF -- which are then forwarded as-is to the
downstream
client / intermediary cache, enabling HTTP response splitting / smuggling.
This is the same primitive as CVE-2026-33523 (the reason phrase is read by the
same line reader, so a raw LF cannot be injected either), in a code path the
original fix did not reach.
== Affected / tested ==
- Apache 2.4.x including 2.4.68 (current). Verified dynamically against the
unmodified official httpd:2.4 image (Apache/2.4.68 (Unix)).
- Threat model: a reverse (or forward) proxy in front of an untrusted or
compromised backend -- identical to CVE-2026-33523 and the recent
mod_proxy_ajp issues.
== Root cause ==
Final-response path (sanitized) -- modules/http/http_filters.c,
validate_status_line():
/* Check for newlines and control characters */
if (len > 4 && *ap_scan_http_field_content(r->status_line + 4)) {
r->status_line = NULL; /* -> replaced with the canonical reason
phrase */
return APR_EGENERAL;
}
Interim-response path (NOT sanitized) -- server/protocol.c,
ap_send_interim_response():
status_line = r->status_line;
if (status_line == NULL) {
status_line = ap_get_status_line_ex(r->pool, r->status);
}
response_line = apr_pstrcat(r->pool,
AP_SERVER_PROTOCOL " ", status_line, CRLF,
NULL);
...
ap_fputs(x.f, x.bb, response_line); /* written straight to the client */
Attacker-controlled data reaches r->status_line in
modules/proxy/mod_proxy_http.c:
the backend status line is read with ap_proxygetline() -> ap_rgetline_core(),
which
strips only the trailing CRLF (embedded control characters other than LF
survive),
then:
proxy_status_line = apr_pstrdup(p, &buffer[9]);
r->status = proxy_status;
r->status_line = proxy_status_line;
...
if (ap_is_HTTP_INFO(proxy_status)) { /* 1xx */
...
ap_send_interim_response(r, 1); /* forwarded unsanitized */
}
With the default interim policy (no "proxy-interim-response" set) the 1xx
response
is forwarded, and for any 1xx other than 100 (e.g. 103 Early Hints) it is sent
to
any HTTP/1.1 client regardless of Expect: 100-continue.
Note: the HTTP/2 backend path (modules/http2/h2_proxy_session.c) is NOT
affected,
because it synthesizes r->status_line from canonical strings.
== Proof of concept ==
Reverse proxy "ProxyPass / http://backend/" in front of a backend that returns,
for any request, a 103 whose reason phrase embeds a bare CR + an injected
header,
then a normal 200. On the wire the proxy sends to the client:
HTTP/1.1 103 Early Hints<0x0D>X-Injected-By-Backend: SMUGGLED<0x0D><0x0A>
i.e. the bare CR (0x0D) and the injected header are forwarded verbatim inside
the
1xx status line. A downstream agent that treats a bare CR as a line terminator
parses "X-Injected-By-Backend: SMUGGLED" as a response header (response
splitting
/ cache poisoning).
Control case (same poison in a 200 final status line) is correctly sanitized by
validate_status_line() and emitted as "HTTP/1.1 200 OK", confirming the
inconsistency. A self-contained Docker PoC (malicious backend + httpd:2.4 +
raw-socket client) and a captured hexdump are attached.
== Suggested fix ==
Sanitize the status line in ap_send_interim_response(), mirroring
validate_status_line(): fall back to the canonical reason phrase when the
provided status line contains anything other than valid HTTP field-content.
The legitimate callers (core Upgrade handling, 100-continue, mod_proxy_http2)
all use canonical status lines and are unaffected. A patch against
server/protocol.c is attached:
if (status_line == NULL
|| *ap_scan_http_field_content(status_line)) {
status_line = ap_get_status_line_ex(r->pool, r->status);
}
Additionally, the header values forwarded by ap_send_interim_response(r, 1)
(the
send_header callback writes r->headers_out without a control-character check)
carry the same bare-CR primitive and may warrant equivalent hardening.
== Disclosure ==
This is not yet public. I will follow your coordinated-disclosure timeline and
will not open any public issue/PR until you have addressed it. Happy to provide
any further detail or testing.
Best regards,
Francisco jose Gutierrez
--
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]