...

This App’s Own Security Feature Was Helping Attackers Bypass It: Reflected XSS via CSP Nonce Injection

Rohit P.

How a Single </script> Tag Broke an Entire Security Policy

A classic </script> breakout in a JSON config block would normally be stopped cold by a properly enforced Content-Security-Policy. Here, the CSP’s own nonce-injection middleware handed the injected script a valid nonce  turning a defense into an accomplice.

What We Found

The affected endpoints take user input straight from the query string and reflect it  unsanitized  into a JSON configuration object embedded inside an inline <script> block. Because the value isn’t context-encoded, an attacker can inject a literal </script> sequence. The HTML parser doesn’t care what JavaScript string context that sequence sits inside; it closes the script element right there, no matter what.

Once the original script block is closed, the attacker’s payload can open a brand-new <script> element with whatever JavaScript they want, and it executes in the victim’s authenticated origin. That part alone is a textbook reflecting XSS. What makes this one worth writing up is what happened next.

⚠ THE TWISTThe application runs CSP nonce-injection middleware that rewrites every <script tag in the outgoing HTML to attach a valid nonce  including the attacker’s injected one. A correctly enforced, non-report-only CSP ends up approving the exact script it was supposed to block.

FIG.1  The moment CSP flips from defense to accomplice. The middleware nonces every script tag indiscriminately, attacker-injected ones included.

Steps to Reproduce

1. Authenticate to the application. Any user role is sufficient; the vulnerable endpoints don’t require elevated privileges, just an authenticated session.

2. Request the vulnerable endpoint with a breakout payload. A query string containing </script><script>…</script> is reflected directly into the inline configuration block.

3. The injected script executes  nonce and all. The CSP nonce-injection middleware attaches a valid nonce to the injected tag automatically, so the browser runs it without complaint.


# Authenticated request with a breakout payload in the query string
$ curl -b "redacted-auth=<cookie>" \
  'https://redacted.com/ReportDesigner/NewReport?%3C%2Fscript%3E%3Cscript%3Ealert(%27XSS%27)%3C%2Fscript%3E'

# Decoded payload for readability:
</script><script>alert('XSS')</script>

Response: injected <script> tag receives a valid CSP nonce
→ alert('XSS') executes in the authenticated session

Fig: Confirming the injected script executed.

Root Cause: Two Bugs Stacked, Not One

The first bug is unremarkable on its own: user input lands inside a JSON blob inside an inline <script> tag without being encoded for that context. Escaping a value for a JavaScript string doesn’t help if the value also contains </script>  the HTML parser closes the tag before the JavaScript engine ever gets a say. This is a well-known class of bug, and it’s exactly why context-aware output encoding matters: you have to encode for every layer the value passes through, not just the innermost one.

The second bug is the one that turns this from routine XSS into something worth a design review. The application’s CSP nonce middleware exists specifically to stop injected scripts from running  but it works by scanning the final HTML for <script> tags and stamping a valid nonce onto anything it finds, with no concept of “this tag came from user input, don’t trust it.” So the very defense meant to catch this exact scenario ends up handing the attacker a matching key.

FIG.2  The safety net had a hole exactly where it mattered. A CSP is only as good as the middleware that decides who gets a nonce.

Add ‘unsafe-eval’ sitting in the script-src directive and a missing default-src / img-src restriction, and the policy was weakened on two more fronts at the same time  neither of which caused this bug, but both of which reduce how much protection was actually left once the nonce check stopped meaning anything.

Recommendations

Context-encode everything reflected into the inline script block. When a value goes into a JSON object embedded inside <script>, JSON-encodes it and escapes <, >, &, and / so a literal </script> can never appear in the output.

Make the CSP nonce middleware source-aware. A nonce should never be handed to a tag just because it matches a pattern:

  • Whitelist only known, application-generated <script> tags  by source location or a hardcoded list
  • Never nonce a <script> tag that originates from URL parameters, form data, or any other user-controlled input
  • Log and alert when an attempted injection is detected  a strong defense-evasion signal worth watching

Remove ‘unsafe-eval’ from script-src. It doesn’t cause this particular bug, but it removes one more thing standing between “script executes” and “script does whatever it wants.”

Set explicit default-src and img-src directives. Closing the image-based exfiltration channel doesn’t fix the XSS, but it meaningfully narrows what a successful injection can actually do.

The Bigger Picture

A Content-Security-Policy is only as trustworthy as the code that decides what gets a nonce. It’s tempting to treat “we have CSP” as a checkbox that neutralizes XSS findings  but a nonce-injection layer that can’t distinguish first-party markup from reflected user input isn’t providing defense in depth, it’s providing a false sense of one. The underlying output-encoding bug still needs fixing regardless; the CSP failure just meant there was nothing behind it when it mattered.

A security control that can’t tell trusted content from attacker content isn’t a control, it’s a formality.

Leave a Reply