XSS Prevention — Angular’s Security Model and Safe HTML Handling

Angular has a built-in XSS defence system: by default, all values bound to the DOM are treated as untrusted and sanitised. String interpolation ({{ value }}) HTML-encodes the output. Property binding ([property]="expr") sanitises based on the property type. The only way to bypass Angular’s sanitisation is via the DomSanitizer bypass methods — which should be used sparingly, only with known-safe content, and never with unprocessed user input.

Angular’s Security Model

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

// ── Angular's automatic XSS protection ────────────────────────────────────

// ✅ SAFE — Angular HTML-encodes the output
// If userInput = '<script>steal()</script>', the template renders:
// <p>&lt;script&gt;steal()&lt;/script&gt;</p> (harmless text)
// <p>{{ userInput }}</p>

// ✅ SAFE — Angular sanitises [innerHTML] by removing dangerous elements
// Angular strips <script>, event handlers (onclick), javascript: URLs
// <div [innerHTML]="userContent"></div>
// userContent = '<b>Bold</b><script>evil()</script>'
// Rendered: <div><b>Bold</b></div> — script stripped

// ⚠️  BYPASS — use only with content YOU construct (not user input)
@Component({ standalone: true, template: `
  <div [innerHTML]="safeHtml"></div>
` })
export class PostContentComponent {
  private sanitizer = inject(DomSanitizer);

  // Server-generated HTML we trust — marked as safe explicitly
  @Input() set html(value: string) {
    // Only do this for HTML from YOUR server, not from user-provided content
    this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(value);
  }
  safeHtml!: SafeHtml;
}

// ⛔ NEVER DO THIS — user content bypassed without sanitisation
// this.sanitizer.bypassSecurityTrustHtml(userComment);  // XSS vulnerability

// ── SafeHtmlPipe — for server-generated content ────────────────────────────
@Pipe({ name: 'safeHtml', standalone: true })
export class SafeHtmlPipe implements PipeTransform {
  private sanitizer = inject(DomSanitizer);

  transform(html: string | null): SafeHtml {
    if (!html) return '';
    // Angular will sanitise this — script tags, event handlers removed
    // bypassSecurityTrustHtml NOT used here — let Angular sanitise
    return html;  // Angular sanitises [innerHTML] binding automatically
    // Use bypass only if you need to render <iframe>, <script>, etc. from trusted source
  }
}

// ── URL sanitisation ──────────────────────────────────────────────────────
// Angular strips javascript: URLs from [href] and [src] automatically
// <a [href]="userUrl"> — Angular checks the URL, blocks javascript: protocol
// SafeResourceUrl needed for <iframe src> and <script src> from trusted sources:
// this.sanitizer.bypassSecurityTrustResourceUrl('https://trusted.com/embed')

// ── Content Security Policy — defence in depth ────────────────────────────
// Add to nginx config for the Angular app:
// add_header Content-Security-Policy "
//   default-src 'self';
//   script-src  'self';              ← blocks inline scripts and external scripts
//   style-src   'self' 'unsafe-inline';   ← allows inline styles (Material needs this)
//   img-src     'self' data: https:;
//   connect-src 'self' https://api.blogapp.com;
//   frame-ancestors 'none';          ← prevents clickjacking
// " always;
Note: Angular’s sanitisation for [innerHTML] uses an allowlist of safe HTML elements and attributes. It keeps: common formatting tags (<b>, <i>, <p>, <ul>, <a href>), image tags with safe src, and standard layout elements. It strips: <script> and <style> tags, event handler attributes (onclick, onerror), javascript: protocol in URLs, and <iframe>. This built-in sanitisation handles the most common XSS vectors without needing manual escaping for server-rendered HTML content.
Tip: Implement a Content Security Policy (CSP) on the web server that serves your Angular application. A CSP that blocks script-src to only your own origin means that even if an attacker injects <script src="https://evil.com/xss.js"> into the DOM, the browser refuses to execute it. CSP is a defence-in-depth measure — it does not replace Angular’s built-in sanitisation but significantly raises the bar for successful exploitation. Test your CSP with the CSP Evaluator tool from Google.
Warning: The Angular documentation warns that bypassSecurityTrustHtml() is a red flag in code reviews. Every use should be justified in a comment explaining why the content is trusted. A common dangerous pattern: storing HTML in the database from a rich text editor and rendering it with bypass. If the rich text editor is not properly configured to sanitise on input, or if the database is compromised, an attacker can inject HTML with JavaScript that bypass will faithfully render. Always sanitise on input (when the user submits content) and consider sanitising again on output.

Common Mistakes

Mistake 1 — Using bypassSecurityTrustHtml with user-provided content (XSS vulnerability)

❌ Wrong — sanitizer.bypassSecurityTrustHtml(userComment); malicious user injects JavaScript that runs for all viewers.

✅ Correct — never bypass with user content; use [innerHTML]="userContent" (Angular sanitises automatically) or a server-side HTML sanitiser.

Mistake 2 — Relying solely on Angular sanitisation without CSP (single layer of defence)

❌ Wrong — Angular sanitises but no CSP; a bypass in a dependency or a library allows script injection.

✅ Correct — add CSP as a defence-in-depth layer; blocks injected scripts even if sanitisation is bypassed.

🧠 Test Yourself

A template has <div [innerHTML]="post.body"></div> where post.body contains <script>alert('XSS')</script>. What does Angular render?