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]

Reply via email to