Search for the escape hatches that skip each framework's escaping: dangerouslySetInnerHTML in React, v-html and innerHTML in Vue, and innerHTML bindings plus the bypassSecurityTrust methods in Angular. Add direct DOM access, string templates and eval. Each hit needs a known, trusted or sanitized source.
Definitions
A sink is a place where data becomes live page content: HTML, a URL, a style or code. XSS happens when attacker-controlled data reaches a sink without being encoded or sanitized for that context. Modern frameworks make the default sinks safe and leave a short list of deliberate ones.
OWASP's JavaScript and TypeScript cheat sheet names the main bypasses that must never receive untrusted input: React dangerouslySetInnerHTML, Vue v-html and Angular bypassSecurityTrustHtml and its siblings. It also advises building DOM with textContent and createElement instead of innerHTML or insertAdjacentHTML.
The review table
| Sink | Framework | What it does | Review question |
|---|---|---|---|
| dangerouslySetInnerHTML | React | Overrides innerHTML with a raw HTML string | Where does the string come from, and is it sanitized? |
| v-html or innerHTML prop | Vue | Renders raw HTML, bypassing escaping | Is the source trusted or sanitized? |
| Binding to innerHTML | Angular | Interprets HTML but Angular still sanitizes it | Is a bypass call also involved? |
| bypassSecurityTrust methods | Angular | Tells Angular to stop sanitizing | Is the value fully controlled, and the context correct? |
| href and src from data | All | Can carry a javascript: URL | Is the scheme and host validated? |
| Direct DOM through refs or ElementRef | All | Skips framework protection | Does it use innerHTML or insertAdjacentHTML? |
| Template built from strings | Vue, Angular | Compiles attacker text as code | Is user text ever in a template? |
| eval, new Function, string timers | All | Runs code from strings | Can any input reach them? |
| Style bindings from users | All | Enables clickjacking style tricks | Are only specific properties allowed? |
How to run the review
- Grep for each sink name from the table across source and templates.
- For every hit, trace the value back to its origin and label it: constant, trusted internal, sanitized, or unknown.
- Fix unknowns first: replace with text rendering, or sanitize with a maintained library and do not edit afterwards.
- Add lint rules for the most common sinks. The react/no-danger rule and Vue's vue/no-v-html rule exist for this, and both can be disabled where content is known to be sanitized.
- Add the no-eval, no-implied-eval and no-new-func rules that OWASP recommends.
- Add a pull request prompt: "Did this change add or touch a sink from the table?"
Defense in depth
Frameworks guard the DOM. A content security policy and Trusted Types guard it from outside the framework. The Angular guide says these work at the DOM level, the most effective place to prevent XSS, where they cannot be bypassed by lower level APIs. For Trusted Types in Angular, see the policy names explained.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how each finding records a file and line, like one row in the sink review above.
Safeguard's free scan at gold.safeguard.sh/scan covers dependencies, which also carry sinks of their own.
Quick check
Is innerHTML always bad?
It is a sink. It is fine with trusted constants and risky with anything an attacker can influence. Prefer textContent when you only need text.
Do lint rules replace review?
No. They find the sinks. A person still decides whether each source is safe.
Sources
- OWASP JavaScript and TypeScript Security Cheat Sheet
- OWASP Cross Site Scripting Prevention Cheat Sheet
- Angular security guide
- Vue security guide
- React reference: common components
Last reviewed: 19 October 2026.