Writing something after such a long time. While I wrote the last post with some desperation, I really wanted to elaborate on something interesting I discovered, in some clean fashion. I submitted a couple of nice escalations to Cloudflare in the past couple of months and this one talks about one of them.
One fine evening after finishing work at daytime, I noticed Kenny’s post about Cloudflare’s new MCP Server Portal feature. I already knew something was cooking about it through the JS analysis a few days ago, but didn’t care enough before it came to reality. This post revolves around a simple OAuth misconfiguration & what your deep-target recon can turn from a Single App to an Entire Organization Takeover.
Preparation: WTH is MCP Server Portal
MCP (Model Context Protocol) Server Portal is a new feature in Cloudflare One that allows organizations to expose MCP servers through a centralized portal, in addition to various fine-grained access control policies.
It works by registering a new (sub)domain for MCP Server Portal (I’ll refer to it as MSP for short in the rest of the post), say mcp.rookie.com, and Cloudflare will internally handle OAuth for this domain and protect access to various MCP Servers exposed on this domain. Typical use cases involve restricting MCP Tools for users based on their roles, governing their usage, etc.
Opening: Second-Order XSS in MSP
While exploring the OAuth implementation of MSP, I noticed that the /register endpoint accepts a redirect_uris parameter during client registration.
The OAuth flow in MSP looks like:
sequenceDiagram
participant Client as MCP Client
participant MSP as MCP Server Portal<br/>mcp.rookie.com
participant CF as Cloudflare Access<br/>team.cloudflareaccess.com
Client->>MSP: 1. Initiate auth at /mcp
Client->>MSP: 2. POST /register with redirect_uri
MSP-->>Client: client_id
Client->>MSP: 3. GET /authorize?client_id&redirect_uri
alt No CF_Authorization cookie
MSP->>CF: Redirect to Cloudflare Access
CF->>Client: Login page
Client->>CF: Authenticate
CF->>MSP: Redirect back with session
end
MSP-->>Client: 4. Redirect to /authorize?code&client_id&redirect_uri
MSP-->>Client: 5. window.location.href = redirect_uri
It was quite naive to discover that there was no validation on this parameter: it accepts any string input.
POST /register HTTP/1.1
Host: mcp.rookie.com
Content-Type: application/json
{
"redirect_uris": ["random.example"],
"client_name": "Malicious Client"
}
The server happily registers this malicious redirect URI and returns a client_id in the response:
{
"client_id": "malicious-client-id-here",
"redirect_uris": ["random.example"]
}
After couple of OAuth redirects, the final code is generated on /authorize endpoint and redirected back to the client using window.location.href: a known XSS Sink. The first attempt to use javascript: URI worked, meaning I can execute any javascript code on the MSP domain.
Here’s the actual payload I used:

To sum up, I can create a malicious redirect_uri, associated with a server accepted client_id in the client registration part and then use this client_id in the later mcp.rookie.com/authorize endpoint to trigger the XSS.
The XSS looked like:

This is a classic Second-Order XSS - the payload is stored during registration and triggered during authorization.
Executing JS on MSP was easy. Now, I focused on finding some real impact. I could see the App-scoped CF_Authorization cookies are non-HTTPOnly (however, they can be fine-tuned in the Cloudflare dashboard), susceptible to stealing via XSS. Also, I could interact with the MSP – the tools it has access to, and the other implications from it.
I spent some more time trying to increase the impact to other associated domains, but couldn’t find anything immediately, so I reported the initial finding to their security team.
Mid Game: From Single App to Org-Wide ATO
This is the most interesting and complex part of all.
To understand the full exploit chain, here’s a mini show on how the Cloudflare Access authentication works:
sequenceDiagram
participant User
participant App as Protected App<br/>app.rookie.com
participant CF as Cloudflare Access<br/>team.cloudflareaccess.com
participant IdP as Identity Provider<br/>(Google/Okta/etc)
User->>App: 1. Access protected resource
alt No CF_Authorization cookie
App->>CF: 2. Redirect to Access login
CF->>User: 3. Show login options
User->>CF: 4. Select identity provider
CF->>IdP: 5. Redirect to IdP login
User->>IdP: 6. Authenticate
IdP->>CF: 7. Return with SAML/OIDC assertion
CF->>CF: 8. Validate & create session
CF->>User: 9. Set CF_Authorization cookies:<br/>- Domain-specific (app.rookie.com)<br/>- Org-wide (*.cloudflareaccess.com)
CF->>App: 10. Redirect back to app
end
App->>App: 11. Validate CF_Authorization
App->>User: 12. Grant access to resource
The critical insight here is that Cloudflare sets TWO types of cookies:
- Domain-specific cookie: Valid only for the specific application domain
- Org-wide cookie: Valid for
teamname.cloudflareaccess.comand can be exchanged for access to ANY app in the organization
I had previously discovered Account Takeovers of Cloudflare Access Organizations by stealing the org-wide CF_Authorization cookie (using another patched vulnerability), which was set on the team.cloudflareaccess.com domain. This cookie essentially allows access to every single application that the user is permitted to access.
Recalling from the previous vulnerability, I was attempting to mimic the flow used by the cloudflared CLI tool for org token exchange and transfer the App-specific and org-wide CF_Authorization cookie to the device initiating the attempt. The important thing to notice from this flow was that whenever the redirect_uri in the Cloudflare Access login flow contains isCLI=true and send_org_token=true parameters, the redirected URL contains an additional orgNonce parameter; otherwise, the URL only contains the nonce parameter. If I could get this orgNonce somehow, it would be possible to exchange it for the org-wide CF_Authorization cookie and hence achieve org-wide ATO!
Challenge 1: Getting the orgNonce
When the CF_Authorization cookie is present on teamdomain.cloudflareaccess.com (i.e., an authenticated victim session), passing these additional parameters in the redirect_uri param on Step 2 redirects directly to the MSP endpoint at mcp.rookie.com/cdn-cgi/access/authorized?nonce=…&orgNonce=…. When app-specific CF_Authorization cookies are also present, the subsequent redirect doesn’t include the orgNonce. I found out that if at this point App cookies are absent in the request headers, App-specific CF_Authorization cookies are specifically set in the response headers (because we’re on the mcp.rookie.com/cdn-cgi/access/authorized?nonce=… endpoint) and then it redirects to another MSP endpoint with orgNonce present in the query param. Since we have XSS on the MSP domain, I can now steal the orgNonce.
This approach requires me to delete/remove the App-specific CF_Authorization cookie on MSP domain after gaining XSS on MSP domain. Later stealing was possible because MSP’s cookie will be set again when absent and hence the subsequent requests will be authenticated.
Challenge 2: Can’t Delete HTTPOnly Cookies
The cookies on the MSP domain can be HTTPOnly (configurable from the Cloudflare dashboard), so I couldn’t delete or override them via JavaScript. I needed another way to invalidate the domain-specific session.
I tried the documented way to log out a session – mcp.rookie.com/cdn-cgi/access/logout, but this invalidates cookies on teamname.cloudflareaccess.com as well (because they’re tied to the same session identifier), which breaks the chain since we need the org-wide session to remain valid.
The Breakthrough: Domain-Specific Cookie Invalidation
After extensive testing, I discovered an unusual URL that invalidates only the MSP domain-specific CF_Authorization cookie: https://mcp.rookie.com/cdn-cgi/access/login/mcp.rookie.com
A simple intermittent redirection to this endpoint clears the cookies on the MSP domain without affecting the session on teamname.cloudflareaccess.com.
The Final Chain
With the orgNonce generated and transferred to an MSP endpoint (post 3-4 automatic redirects), I can access that full URL by virtue of XSS on MSP (same origin policy allows it) and hereby retrieve the orgNonce.
Here’s the complete attack chain:
flowchart TB
subgraph "Attack Setup"
A1[Attacker registers malicious OAuth client<br/>with javascript: redirect URI]
A2[Generate authorization URL<br/>mcp.rookie.com/authorize?client_id=xxx]
end
subgraph "Victim Interaction"
V1[Victim clicks link<br/>Single click required]
V2[XSS triggers on mcp.rookie.com]
end
subgraph "Exploit Chain via XSS"
E1[Invalidate MSP cookie via<br/>mcp.rookie.com/cdn-cgi/access/login/mcp.rookie.com]
E2[Navigate to teamname.cloudflareaccess.com/cdn-cgi/access/login/mcp.rookie.com?redirect_uri=...<br/> And finally redirected to mcp.rookie.com/cdn-cgi/access/cli?orgNonce=...]
E3[Extract orgNonce from URL<br/>Now visible due to initial missing domain cookie]
E4[Exchange orgNonce for<br/>org-wide CF_Authorization JWT]
end
subgraph "Impact"
I1[Access ALL Cloudflare Access apps<br/>in victim's organization]
I2[Steal MCP server data]
I3[Access internal WARP/VPN resources]
end
A1 --> A2 --> V1 --> V2 --> E1 --> E2 --> E3 --> E4 --> I1
I1 --> I2
I1 --> I3
Impact
With the org-wide CF_Authorization cookie, an attacker can:
- Access ALL Cloudflare Access protected applications in the victim’s organization like Jira, Slack, etc…
- Impersonate the victim across all apps - Self-Hosted, SaaS, Private/Internal
- Access sensitive data from MCP servers which might hold internal information like internal documentation
- Potentially access WARP/VPN protected internal networks
I wanted to create a single click exploit, so the XSS performed the entire “logout-login” dancing and finally the orgNonce stealing.
End Game: Multiple Patch Bypasses through double-quote escaping & JS tricks
Cloudflare patched the above issue by restricting URLs with http/s protocol in the redirect_uris parameter. I found a bypass by escaping the double-quotes and injecting dangerous JS directly into the script tag. Quotes in the Host part of the URL weren’t escaped, but Path/Query Params were working as intended. A few blacklisted characters were causing issues due to some URL validations, but some JavaScript tricks were enough to bypass them. Once XSS is proved, the same impact can be achieved.
A few weeks later, I found another bypass, surprisingly through the same “javascript” protocol, but using a case-sensitive exploit. I suspect this was a regression of the first vulnerability, since it was tested by me before. Though this issue isn’t reproducible anymore, the H1 report is still open and unrewarded.
During my last review, Cloudflare has changed the whole design pattern and added a new interstitial window before final redirection, thus fixing all of the plausible issues.
Timeline
| Date | Event |
|---|---|
| Aug 27, 2025 | Initial report submitted - XSS leading to MCP Portal ATO |
| Aug 27, 2025 | Escalation submitted - Single App XSS to Org-Wide ATO |
| Sep 05, 2025 | Report triaged by HackerOne |
| Sep 11, 2025 | Severity downgraded to Low |
| Sep 12, 2025 | Cloudflare responds - Low severity due to “phishing requirement” |
| Sep 15, 2025 | $450 bounty awarded :) |
| Oct 24, 2025 | Bypass found and submitted |
| Nov 5, 2025 | Bypass patched and awarded $400 bounty |
| Nov 24, 2025 | Regression reported |
| Dec 11, 2025 | Disclosure approved with suggestions |
| Dec 16, 2025 | Writeup published |
Conclusion
What started as a simple observation about a new feature announcement turned into a full org-wide Account Takeover chain. The key was not stopping at the initial finding (XSS with HTTPOnly cookie limitations) but pushing further to find creative ways to escalate the impact.
Oh yeah, I realized that I’ve climbed to top spot recently 😉

That’s all for this post. Stay curious ✌️.