Smuggling Through the Front Door... Achieving 0-Click XSS with Cache Poisoning
In this post, I want to walk through a new request smuggling bug chain I reported to MSRC that affected Azure Front Door. This issue was tracked as VULN-157984, confirmed by Microsoft, fixed, and awarded under the Azure bounty program.
In the previous post, I wrote about VULN-152925, a request smuggling to cache poisoning issue affecting Azure Front Door. That first report let me poison redirects so unrelated users could be sent to a different Azure Front Door-backed host. Microsoft confirmed the issue, fixed it, and awarded a bounty.
While the first case was still open, I mentioned another behavior I was seeing: the same general class of desync/cache issues could also place attacker-controlled HTML into Azure's error responses. At the time, I did not want to spam the case too much, and the main report was already focused on redirect poisoning.
After Microsoft reported that the first issue was fixed, I retested. The original redirect path looked better, but the HTML injection path was still alive. Even worse, I found a second request shape that did not need the exact same malformed POST primitive. This one used a malformed or confusing GET request, a smuggled Host header, and Azure's own error page rendering.
That became VULN-157984, a bypass of the first fix. MSRC confirmed it, fixed it, and awarded another $10,000 bounty payment.
This was a different bug than the redirect poisoning issue, but it came from the same area of the platform: parser inconsistencies at the edge, followed by response generation that trusted data from the wrong request.
Why This Was a Separate Report
During the first MSRC case, I had already started noticing that not every impact was a redirect. Some targets would turn the smuggled data into a 400 Bad Request or "service unavailable" page. That alone is not always interesting, but the error page included the host value.
400 domain.com is facing issues.
It looks like there is a bad request.
That meant if I could control the host value of the smuggled request, and if that value was reflected as HTML, I could potentially turn a desync into XSS.
Near the end of the first ticket I sent MSRC a note explaining that I had seen HTTPS targets where I could cache an XSS payload for the next user. After the first patch, I tested this behavior again and found that the redirect issue was not the whole story. The XSS path still worked.
This is why I treated it as a new report:
- the request primitive was different
- the impact was different
- the first fix did not stop it
- the issue survived on multiple Azure Front Door-backed customer properties
- in some cases it also worked when testing HTTP/2-facing targets
The first bug was traffic redirection. This second bug was a zero-click XSS delivered through cache poisoning.
The Bypass Request
The first clean reproduction used my own Azure Front Door endpoint:
https://d3d-ejdwaudvfxgqfkbq.z01.azurefd.net/
The request looked like this:
GET / HTTP/1.1 Host: d3d-ejdwaudvfxgqfkbq.z01.azurefd.net Accept-Language: en-US,en;q=0.9 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Connection: keep-alive Content_Length: 81 GET / HTTP/1.1 Host: <IMG SRC=# onmouseover="alert(document.domain)"> Smuggle:
The weird part here is the header:
Content_Length: 81
That is not a normal Content-Length header. The underscore matters. In some cases I could also reproduce variants with a normal Content-Length, but this malformed form gave me a clean way to explain the bypass. The request is a GET, but it still carries what looks like a second request after the headers.
The smuggled request contains the attacker-controlled host:
GET / HTTP/1.1 Host: <IMG SRC=# onmouseover="alert(document.domain)"> Smuggle:
Again, the trailing Smuggle: header had a single trailing space and no final line ending. As with the first bug, tooling normalization could destroy the exact byte shape needed to reproduce it.
Normal First, Then Broken
On the first send, the response from my Azure Front Door endpoint looked normal. In my test setup, the endpoint redirected to example.com, so a normal response body showed the Example Domain page:
HTTP/1.1 200 OK Date: Fri, 04 Jul 2025 21:00:23 GMT Content-Type: text/html Content-Length: 1256 Cache-Control: max-age=2279 x-azure-ref: 20250704T... X-Cache: TCP_HIT <!doctype html> <html> <head> <title>Example Domain</title>

But when I pressed send twice back to back, the response changed from a normal 200 into a 400. More importantly, the injected Host header appeared inside the generated error page as HTML:
<h1>400</h1> <h2> <img src=# onmouseover="alert(document.domain)"> <span> is facing issues</span> </h2>

At that moment the bug became much more interesting. I was no longer only controlling a redirect header. I was controlling HTML inside an error response generated by the platform.
If this response could be cached or served to the next user, then this was not just reflected XSS against my own malformed request. It was a zero-click XSS delivered through a poisoned response.
Moving From My Host to a Real Victim Flow
The next step was to test a target where the behavior affected another user-style request.
One of the examples I used was:
https://e-[redacted].com/
The proof-of-concept request was the same idea:
GET / HTTP/1.1 Host: e-[redacted].com Accept-Language: en-US,en;q=0.9 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Connection: keep-alive Content-Length: 81 GET / HTTP/1.1 Host: <IMG SRC=# onmouseover="alert(document.domain)"> Smuggle:
In this case I used a normal Content-Length value rather than the underscore version. That was another reason I did not want to describe this as a one-off header typo. The exact parser disagreement varied by target, but the backend result was the same: the second request’s Host value landed in the error page.

To simulate a separate victim, I again used a remote loop:
for i in {1..100}; do curl -k -sI https://e-[redacted].com/ && sleep 2; done
While the loop was running on a remote VPS server, I went back and sent multiple requests with the malformed GET gadget, and after about 5 or 6 requests I could see the global cache was poisoned with the 400 error.

When the page was opened in a browser, the payload executed:

This was the zero-click part. The victim did not need to submit a form, click a malicious link with a payload in the URL, or interact with attacker-controlled content first. They only needed to request the affected host during the poisoned window and receive the cached/generated error page.
I did not need anything fancy for the final proof. A simple alert(document.domain) was enough to prove script execution in the victim origin, and since this bug was affecting so many companies using Azure Front Door, it was better to get this bug patched sooner than later.
Why This Was More Than Reflected XSS
If the only affected response was the response to my malformed request, this would have been much less interesting. I would be sending a broken request with a malicious Host header and getting back an error containing that same value.
That is not what made the report bounty-worthy.
The important behavior was that the malformed request poisoned the response path for another client. The remote loop and browser proof showed that a normal user-style request could receive the error page containing the payload.
This made the issue behave like stored XSS, even though I was not storing content in the target application database. The "storage" was the platform/cache/desync layer:
attacker sends malformed request -> platform generates poisoned 400 response -> poisoned response is served to unrelated user -> user's browser executes HTML/JavaScript under the affected host
That distinction matters because many triage teams will initially see a Host header XSS and think it is self-reflection. In normal circumstances, a malicious Host header reflected into an error page might be low impact because the attacker controls the request that triggers it.
With cache poisoning and desync behavior, the attacker controls one request and the victim receives another response.
That is the whole game.
Wider Testing
I found the behavior across a large number of Azure Front Door-backed customer properties. Many of them appeared to be used by large security companies in Office 365 proxy-style customer environments.
I shared examples with MSRC showing that the valueprefix-style gadget affected many hardened hosts. I also found examples outside that network.
The same exact byte-level trick did not always work everywhere. On some targets, the underscore version of Content_Length helped. On others, a normal Content-Length was enough. On some, the target was HTTP/1.x-facing. On others, the front-facing behavior was HTTP/2.
Another example was found on some security team infrastructure:
https://cdev.renxt.[redacted].com/
That one was especially interesting because the target was HTTP/2-facing in Burp:
GET / HTTP/2 Host: cdev.renxt.[redacted].com Accept-Language: en-US,en;q=0.9 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Encoding: gzip, deflate, br Connection: keep-alive Content-Length: 81 GET / HTTP/1.1 Host: <IMG SRC=# onmouseover="alert(document.domain)"> Smuggle:
The Connection header is the part that stood out. In HTTP/2, connection-specific headers such as Connection, Keep-Alive, Transfer-Encoding, and similar HTTP/1.x hop-by-hop headers are not supposed to be forwarded as normal request headers. A well-behaved HTTP/2 edge should either reject them or normalize them before the request reaches anything that might parse the request as HTTP/1.1.
So when an HTTP/2-facing target still reacted to a request containing Connection: keep-alive, Content-Length, and an embedded HTTP/1.1 request body, it suggested there was likely an HTTP/2-to-HTTP/1.1 translation boundary somewhere behind the edge. That does not automatically prove a downgrade vulnerability by itself, but it is exactly the kind of boundary where downgrade desync bugs tend to appear.
In other words, the browser-facing side could speak HTTP/2 while the next hop still received something closer to HTTP/1.1. If the front end and the downgraded backend disagreed about whether the bytes after the headers were a harmless body or the beginning of another request, the same smuggling primitive could survive even though the client-facing protocol was HTTP/2.
The remote output showed normal 307 redirects, then a poisoned 400 like the others:

On accessing the site when sending attacks from Burpsuite, same results in 0-Click XSS execution without warning across hundreds if not thousands of Azure Front Door clients across HTTP/1.x and HTTP/2.

Impact
The impact was 0-click XSS through a poisoned error response.
For affected hosts, an attacker could send malformed requests that caused an unrelated user’s request to receive an Azure-generated error page containing attacker-controlled HTML from the smuggled Host header.
That could lead to:
- JavaScript execution in the affected host’s origin
- credential phishing on a trusted domain
- session or token theft where accessible to JavaScript
- user action abuse inside the origin context
- account takeover when chained with application-specific session handling
The exploit did not require a victim to click a crafted URL containing the payload. The victim’s request was clean. The poisoned response was the delivery mechanism.
That is what made the bug feel closer to stored XSS than reflected XSS:
payload source: attacker's malformed request payload delivery: platform/cache/desync behavior payload execution: victim's clean navigation to affected host
MSRC assessed the case as:
Case: 99278 Vulnerability: VULN-157984 Severity: Important Security Impact: Elevation of Privilege Bounty: $10,000
Closing
This second bug is why I do not like stopping at the first patch.
The first fix addressed the redirect poisoning behavior I reported, but the broader bug class still had another sharp edge. Once I retested the platform and focused on the error page path, the impact moved from traffic redirection to JavaScript execution.
The important lesson for defenders is that request smuggling findings should not be patched only at the final observed symptom. If the observed symptom is "poisoned redirect," it is tempting to harden redirect construction and move on. But the underlying issue may still allow poisoning a 400 page, a 404 page, a login redirect, a cache key, or some other response path nobody looked at during the first fix.
The important lesson for researchers is similar: when you find a desync primitive, test more than one sink.
Try redirects. Try error pages. Try invalid hosts. Try valid hosts. Try HTTP to HTTPS redirects. Try non-www to www redirects. Try platform default pages. Try customer-configured origins. Try the same idea through different protocol-facing paths.
In this case, that extra testing turned one fixed Azure Front Door bug into a second confirmed MSRC report.
Comments ()