XSS & Sanitization
Cross-site scripting (XSS) is the most common front-end vulnerability: an attacker smuggles executable markup or script into a page so it runs in another user’s browser, stealing tokens, hijacking sessions, or defacing the UI. Angular’s biggest security advantage is that it treats all values bound into templates as untrusted by default and sanitizes them before they ever reach the DOM. Understanding when that sanitization fires — and when it does not — is the key to writing apps that are safe without sprinkling escaping logic everywhere.
How Angular’s automatic sanitization works
When you interpolate or bind a value into a template, Angular inspects the context in which the value will be used and strips anything dangerous for that context. This is contextual escaping: a string that is harmless as text content may be lethal as a URL or a chunk of HTML, so Angular applies different rules depending on where the value lands.
The protection is automatic. You do not call a sanitizer yourself for ordinary bindings — Angular runs the value through its built-in sanitizer as part of change detection.
import { Component } from '@angular/core';
@Component({
selector: 'app-comment',
standalone: true,
template: `
<!-- Interpolation: always rendered as inert text -->
<p>{{ userComment }}</p>
<!-- Property binding to innerHTML: sanitized HTML -->
<div [innerHTML]="userComment"></div>
`,
})
export class CommentComponent {
// A malicious payload submitted by an attacker
userComment = `Nice post! <img src="x" onerror="alert('xss')"> <script>steal()</script>`;
}
The interpolated <p> shows the raw characters as literal text — the angle brackets are escaped, so nothing executes. The [innerHTML] binding does parse the value as HTML, but Angular’s HTML sanitizer removes the <script> tag and the onerror attribute before insertion.
Output:
<p>Nice post! <img src="x" onerror="alert('xss')"> <script>steal()</script></p>
<div>Nice post! <img src="x"></div>
Interpolation (
{{ }}) is never an XSS vector on its own, because the result is always treated as text. The risk appears only when you bind into contexts that interpret markup, such asinnerHTML.
The four security contexts
Angular’s DomSanitizer recognises four distinct sanitization contexts. Each binding target maps to exactly one of them, and Angular picks the right one automatically.
| Context | Applies to | What sanitization does |
|---|---|---|
| HTML | [innerHTML], [outerHTML] | Removes <script>, event handlers, and other executable markup |
| Style | [style] bindings with raw CSS | Strips CSS that can trigger code (e.g. expression(), dangerous url()) |
| URL | [href], [src] and similar | Blocks javascript: and other unsafe schemes |
| Resource URL | <script src>, <iframe src>, [ngSrc]-loaded resources | Cannot be sanitized; Angular rejects untrusted values outright |
Resource URLs are special: there is no safe way to partially clean a URL that will load and execute code, so Angular refuses the binding entirely unless you explicitly mark the value as trusted.
@Component({
selector: 'app-link',
standalone: true,
template: `<a [href]="profileUrl">View profile</a>`,
})
export class LinkComponent {
// The javascript: scheme is neutralized by the URL sanitizer
profileUrl = 'javascript:alert(document.cookie)';
}
The rendered anchor has its href rewritten to the inert value unsafe:javascript:alert(document.cookie), so clicking it does nothing dangerous, and Angular logs a warning in development mode.
Inspecting and forcing sanitization manually
You rarely need to call the sanitizer directly, but DomSanitizer.sanitize() is available when you process a value imperatively and want the same cleaning Angular applies to bindings. Inject it with the inject() function.
import { Component, inject, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-preview',
standalone: true,
template: `<div [innerHTML]="clean"></div>`,
})
export class PreviewComponent {
private sanitizer = inject(DomSanitizer);
raw = `<b>Bold</b><script>alert(1)</script>`;
// Returns the cleaned string, or null if nothing survives sanitization
clean = this.sanitizer.sanitize(SecurityContext.HTML, this.raw);
}
Output:
<div><b>Bold</b></div>
The <b> tag is preserved because it is safe formatting, while the <script> is dropped.
Server-side rendering and Trusted Types
Angular’s sanitizer runs identically during server-side rendering (SSR) with Angular Universal, so values rendered on the server are escaped before they reach the client — there is no window where an unsanitized blob is shipped in the initial HTML. On modern browsers Angular also integrates with the Trusted Types API: when you enable a Trusted Types Content Security Policy, the browser itself enforces that only Angular-sanitized values are assigned to sinks like innerHTML, giving you defence in depth even against bugs in third-party code.
Avoid the native DOM APIs (
element.innerHTML = ...,document.write,eval) inside components. They bypass Angular’s sanitizer completely. Always go through template bindings so the framework’s protection applies.
Best Practices
- Prefer interpolation (
{{ }}) for displaying user data — it is always safe and needs no extra handling. - Treat every value that originates from a user, URL, API, or storage as untrusted; never assume “internal” data is clean.
- Let Angular sanitize automatically; only reach for the
DomSanitizer.bypass*methods when you have verified the value yourself, and isolate that logic in one auditable place. - Never build templates by string concatenation or set
innerHTMLthrough native DOM APIs — both skip the sanitizer. - Enable a Content Security Policy with Trusted Types in production for a second layer of protection behind Angular’s escaping.
- Keep Angular updated — sanitizer rules and security fixes ship with framework releases.