Skip to content
Go back

[CVE-2025-54571] ModSecurity Content-Type Confusion Exposes Script Source and Enables Stored XSS

Volerion Research

TL;DR
A single malformed HTTP request can trick ModSecurity 2.x into sending two responses. The second response inherits attacker-supplied headers, so JavaScript, JSON or HTML that was supposed to be blocked is sent back as text/plain or any other value the attacker chooses. Users on the page see a raw script instead of an executed one, which means they can read server-side source and, in many contexts, reflect the payload to other visitors for stored XSS. The fix ships in 2.9.12.


1. Summary

CVE ID CVE-2025-54571
Affected Product(s) ModSecurity ≤ 2.9.11 when deployed as an Apache filter (all operating systems)
Volerion Risk Score 7.8 / 10
Exploit Status Public PoC on GitHub issue tracker
CISA KEV No

If an upstream request triggers AP_FILTER_ERROR—for example by declaring an impossible chunk size or posting a body larger than the site allows—ModSecurity still passes the request to Apache after it generates the first HTTP 400 style error page. Apache dutifully tries to continue and emits a second, attacker-controlled response. Because the connection has already moved into an error state, the second response bypasses most header sanitation and inherits whatever Content-Type the attacker specified. That turns blocked inline scripts into downloadable source code and lets malicious JavaScript execute in a victim’s browser when embedded through stored or reflected vectors.


2. Context – Why a WAF vulnerability is more dangerous than it sounds

ModSecurity is not merely a firewall rule set; it is often the last layer of defense protecting raw application output from the public internet. Cloud panels, shared-hosting stacks, industrial gateways and an uncountable number of DIY reverse-proxies ship with version 2.x compiled in. Administrators assume that because ModSecurity sits in front of the web server, the module will enforce security headers no matter what. CVE-2025-54571 breaks that assumption.

A single malformed request allows an attacker to serve content with arbitrary MIME types. That is powerful in two ways. First, it undermines content-sniffing and cross-origin defences the target application relies on. Second, it reveals otherwise hidden code, which is invaluable for reconnaissance against legacy PHP and CGI applications that still embed secrets in source comments or rely on security through obscurity.


3. Technical Details – From filter error to double response

When Apache reads an HTTP request body it streams the data through a chain of filters. ModSecurity inserts itself as one of those filters so it can inspect and, if necessary, block traffic. If a fatal parsing problem occurs, ModSecurity returns an AP_FILTER_ERROR constant to the Apache core, which in turn builds an error response.

The vulnerable path looks roughly like this (simplified):

rc = modsec_process_request_body(ctx);

if (rc == AP_FILTER_ERROR) {
    ap_die(HTTP_BAD_REQUEST, r);   // Emits first response
}

/* ModSecurity incorrectly leaves r->connection->aborted false
 * so execution continues into normal handler selection.
 */

Because the connection state is still “healthy”, Apache continues selecting a content handler, which generates a second full HTTP response. RFC 7230 forbids two responses for one request yet browsers accept the first bytes they see, discard the rest and close the socket. Command-line clients such as curl and many CDN proxies, however, assemble both responses into a single payload. The second response therefore lands in caches and on downstream servers exactly as the attacker sent it, including whichever headers they injected.

Proof of concept

The advisory demonstrates exploitation with a chunked request that advertises a 20-byte chunk yet delivers 19 bytes, forcing a parsing failure:

printf 'POST /index.php HTTP/1.1\r
Host: target\r
Transfer-Encoding: chunked\r
Content-Type: text/plain\r
\r
14\r
{"user":"evil"}\r
0\r
\r
GET /safe.js HTTP/1.1\r
Host: target\r
Content-Type: text/plain\r
\r
' | nc target 80

The server first replies with HTTP/1.1 400 Bad Request, then immediately follows with a full 200 OK that returns the script safe.js as text/plain, exposing its content to whoever made the request. Replace the second request with an inline script containing <script>alert(document.domain)</script> and host it in a public location to weaponise the bug into stored XSS.


4. Impact – Source disclosure, bypassed CSP and reliable XSS

Information disclosure is straightforward: any static or dynamic resource behind ModSecurity can be downloaded as raw text simply by triggering an upstream filter error. JavaScript files, HTML templates and even server-side includes that were supposed to execute are revealed.

Cross-site scripting arises when an attacker forces the server to reflect script into a context rendered by other users. Because the response body originates from the application itself, most CSP and X-Frame policies stay intact, yet the MIME confusion downgrades them. In multi-tenant hosting environments a single malicious user can read neighbours’ PHP files and embed those secrets into a phishing page served by the same host. The CVSS 3.1 score of 8.2 captures the high impact on integrity combined with network exploitability.


5. Remediation – Patch or isolate immediately

Upgrade to ModSecurity 2.9.12. The maintainers added an early connection abort once an error response has been generated and extended regression tests to cover split-response scenarios.

If an in-place upgrade is not feasible you can mitigate in three ways:

  1. Enable Apache’s ErrorHeader unset Content-Type directive for 4xx and 5xx responses. That strips the attacker-controlled header from the second response, at the cost of losing correct MIME types for legitimate errors.
  2. Terminate HTTP/2 and HTTPS on a separate proxy such as nginx or HAProxy and reject any response with duplicate status lines before forwarding.
  3. Enforce strict Referrer-Policy: no-referrer and X-Content-Type-Options: nosniff headers at an outer layer. These do not stop the leak but make XSS exploitation harder.

These measures should be considered temporary. Only the official patch fully closes the hole.


6. Timeline

Date (UTC)Milestone
2025-08-06 00:15CVE-2025-54571 published via GitHub Security Advisory GHSA-cg44-9m43-3f9v
2025-08-06 00:18Volerion completes enrichment and publishes risk score

7. References


About Volerion

Volerion delivers AI-driven enrichment minutes after a CVE goes live. A single call to our REST API returns CVSS 4.0 vectors, exploitability metrics and affected products complete with remediation. Additionally, we offer different scoring profiles, complete with insight into the eight comprehensive categories that make up the final score. Our API is also available in the traditional NVD API 2.0 format, so integration is as simple as swapping hosts. Spend less time parsing CVEs and more time closing them.

How the Volerion Risk Score Fits With CVSS, EPSS and KEV

At the time of writing:

The Volerion Risk Score synthesises these signals with exploit maturity, asset prevalence and remediation effort, resulting in a score of 7.8. That places the issue in our high-risk band, guiding patch prioritisation for workloads that rely on ModSecurity for first-line defence.


Share this post on:

Previous Post
[CVE-2025-50707] ThinkPHP 3 File Inclusion Lets Attackers Execute Arbitrary Code
Next Post
[CVE-2025-50706] From Local File Inclusion to Remote Code Execution in ThinkPHP 5.1