Executive Summary

This write-up details a Reflected Cross-Site Scripting (XSS) vulnerability found in a React application. The vulnerability exists due to the unsafe use of dangerouslySetInnerHTML combined with a Content Security Policy (CSP) that whitelists a domain containing a known script gadget. By chaining an HTML injection to bypass React's sanitization with a specific API call to the whitelisted domain, arbitrary JavaScript execution (in the form of an alert) was achieved.

Source Code Analysis & Identification

1. Identifying the Sink

In a black-box or white-box scenario, the primary indicator of this vulnerability is the application's handling of the ?xss= parameter. Looking at the React implementation:

JavaScript

const [input] = useState("..."); // input comes from URL param
React.createElement("div", { dangerouslySetInnerHTML: { __html: input } })

The use of dangerouslySetInnerHTML is the root injection point. React protects against XSS by default, but this property explicitly tells React to render raw HTML. However, React implements a basic filter even within this property: it strips out <script> tags to prevent simple XSS. It does not, however, strip valid HTML tags like <iframe>, <object>, or <img>.

2. Analyzing the Content Security Policy (CSP)

Upon attempting standard HTML injection (e.g., <img onerror=alert(1)>), execution is blocked by the CSP. Analyzing the response headers via curl -I reveals the following configuration:

Content-Security-Policy: ... script-src 'self' 'wasm-unsafe-eval' https://challenges.cloudflare.com 'nonce-...' frame-src 'self' https://challenges.cloudflare.com; ...

Key Observations:

  • Inline Scripts Blocked: The presence of a nonce and the absence of unsafe-inline means we cannot execute inline JavaScript (e.g., <script>...</script> or <svg onload=...>) without knowing the random nonce.
  • Whitelisted Domain: The policy explicitly trusts https://challenges.cloudflare.com.
  • Frame Control: frame-src restricts nested frames to self and the Cloudflare domain, blocking data: URI frames often used to bypass protections.

The Exploit Chain: Why It Works

The successful exploit requires chaining two distinct bypasses: escaping the React sanitization and bypassing the browser's CSP enforcement.

Step 1: Bypassing React Filtering

Since React removes direct <script> tags, we must use an alternative tag that loads content. An <iframe> using the srcdoc attribute is effective here. React renders the iframe, and the browser then parses the HTML contained within the srcdoc attribute.

Payload Stage 1:

<iframe srcdoc="..."></iframe>

Step 2: Bypassing CSP via Script Gadget

Inside the srcdoc, we are subject to the parent page's CSP. We cannot write inline code. We must load a script from a trusted source.

Using a CSP Bypass Search tool (as shown in), security researchers can identify if whitelisted domains contain "gadgets" — scripts that perform actions based on URL parameters.

The search results indicate that challenges.cloudflare.com hosts the Turnstile API, which accepts an onload parameter.

The Gadget:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=alert"></script>

Why this specific gadget works:

  • Trust: The browser loads the script because https://challenges.cloudflare.com is listed in script-src.
  • Execution: The internal logic of api.js looks for the onload query parameter. It takes the value provided (alert) and executes it as a global function call: window['alert']().
  • Result: This executes alert() within the context of the application, proving XSS.

Final Payload:

<iframe srcdoc="<script src='https://challenges.cloudflare.com/turnstile/v0/api.js?onload=alert'></script>"></iframe>

Root Cause Analysis: Why It Is Vulnerable

The application is vulnerable due to two concurrent architectural flaws:

  • Unsanitized Input in Sink: The application passes user-controlled input directly to dangerouslySetInnerHTML without passing it through a sanitization library like DOMPurify. While React removes script tags, it is not a security sanitizer and leaves other dangerous vectors open.
  • CSP Allow-List Misconfiguration: The CSP relies on allow-listing entire domains (CDNs or APIs) rather than using Strict CSP (using only nonces or hashes). When a domain like Cloudflare, Google Analytics, or UNPKG is whitelisted, any script hosted on those domains can be used. If those scripts have gadget behavior (like executing callbacks defined in the URL), the CSP is effectively bypassed.

Methodology for Real-World Targets

To find similar vulnerabilities in real-world bug bounty engagements, follow this workflow:

  • Source Code Auditing: Search client-side JavaScript bundles for dangerouslySetInnerHTML (React), v-html (Vue), or [innerHTML] (Angular). Verify if the variable passed to these properties is influenced by user input (URL parameters, API responses).
  • CSP Enumeration: Inspect the Content-Security-Policy header. Look for broad allow-lists (e.g., *.google.com, cdnjs.cloudflare.com, unpkg.com).
  • Gadget Scanning: Use tools like CSP Evaluator or CSP Bypass Search (referenced in). Input the target's CSP to automatically check known whitelisted domains for documented JSONP endpoints or script gadgets that allow arbitrary code execution.
  • Context Escaping: If the framework filters specific tags, test alternative HTML elements (iframe, object, embed) that create new execution contexts or allow external resource loading.

Extra:

https://cspbypass.com/

None