skip to content

Sanitization & Trust Bypass

Angular treats every bound value as untrusted and sanitizes it per security context, unless you call a bypassSecurityTrust method or write to the DOM yourself. Interviewers probe both escape routes.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Angular, what happens to a user-supplied string containing HTML when you render it with {{ }} interpolation versus binding it to [innerHTML]?

level: juniorimportance: must knowfreq 74%

answer

  1. text node versus parsed markup
  2. interpolation never parses tags
  3. HTML security context
  4. allow-list of elements and attributes
  5. dev-mode console warning

basics

~20 s

Interpolation writes the string as text, so tags appear literally and nothing runs. [innerHTML] parses it as HTML, but Angular first sanitizes it in the HTML security context, dropping scripts and event-handler attributes and neutralizing javascript: URLs.

solid answer

~40 s

`{{ comment }}` becomes the element's text content, so `<b>hi</b>` shows with its angle brackets and no markup is ever parsed: that is the safe default. `[innerHTML]="comment"` asks the browser to parse markup, which Angular treats as the **HTML security context**, so the compiled binding runs the value through Angular's built-in sanitizer before writing it. The sanitizer works from an allow-list: `<b>`, `<p>`, `<a>` and `<img>` survive, `<script>` and `<style>` are dropped together with their contents, attributes such as `onerror`, `onclick`, `style` and `id` are removed, and a `javascript:` URL in an `href` gets an `unsafe:` prefix. In development mode Angular logs `WARNING: sanitizing HTML stripped some content`; a production build strips silently. The protection belongs to the binding, so writing to the DOM yourself skips it.

code

ts · 12 lines
ts
import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-comment-preview',
  template: `
    <p>{{ body() }}</p>
    <div [innerHTML]="body()"></div>
  `,
})
export class CommentPreview {
  body = signal('<b>Nice post</b><img src="x" onerror="alert(1)">');
}

go deeper

for a junior

Recall the split: interpolation shows text and never parses tags, while [innerHTML] renders markup after Angular's sanitizer has removed scripts, event handlers and dangerous URLs.

for a middle

Explain that the compiler picks the sanitizer from the element and property, and name what the allow-list drops, including style and id, and the dev-mode warning.

for a senior

Point out where the guarantee ends: direct DOM writes and bypassSecurityTrust values skip the sanitizer, so reviews should hunt for those rather than for [innerHTML] itself.

for a principal

Frame interpolation as the default a team standard enforces, with [innerHTML] reserved for vetted markup sources, and weigh the cost of silent stripping in production against visible failures.

## Two ways to put a string on screen An Angular template offers two everyday ways to show a string that came from a user, a database or an API: - **Interpolation**, written `{{ value }}` between tags, puts the value into the page as **text**. - A **property binding to `innerHTML`**, written `[innerHTML]="value"`, hands the value to the browser as **markup to parse**. The two look similar in a template and behave very differently, which is why interviewers use them to open a conversation about cross-site scripting (XSS): an attack in which attacker-supplied data ends up being executed as code in another user's browser. ## What interpolation does Interpolation creates or updates a text node. A text node is never parsed as HTML, so a value such as `<b>hi</b><img src=x onerror=alert(1)>` is displayed character for character, angle brackets included. Nothing is removed and nothing runs. Angular's security guide puts it simply: interpolated content is **always escaped**. This makes interpolation the right default for anything a user typed. If the goal is to show a comment, a name or a search term, interpolation is both correct and safe, and no sanitizer is involved at all. ## What [innerHTML] does: the HTML security context Binding to `innerHTML` exists precisely because sometimes you *want* markup rendered: a formatted help article, a highlighted search snippet, the output of a markdown renderer. Angular cannot simply escape it (that would defeat the purpose), so it **sanitizes** it instead. At compile time the template compiler looks the element and property up in its **security schema**. `innerHTML`, `outerHTML` and an iframe's `srcdoc` belong to the **HTML security context**, so the compiled binding instruction carries a call to the HTML sanitizer. At runtime the value passes through it before the DOM is touched: 1. The string is parsed into an **inert** document, where nothing loads or executes. 2. It is re-parsed until the output stops changing, a guard against **mutation XSS** (markup that the browser's own error correction turns dangerous on a second parse). If it never stabilises, sanitization fails with an error. 3. The resulting tree is walked and re-serialized, keeping only allow-listed elements and attributes. ## What the sanitizer keeps and removes | Input | Result after sanitization | | :--- | :--- | | `<b>`, `<i>`, `<p>`, `<ul>`, `<a>`, `<img>`, headings, tables | kept | | `<script>`, `<style>`, `<template>` | removed **with** their contents | | an unknown element such as `<custom-card>` | the tag is removed, its text content is kept | | `onerror`, `onclick` and every other event-handler attribute | removed | | `style` and `id` attributes | removed (not on the allow-list) | | `class`, `title`, `alt`, `aria-*` attributes | kept | | `href="javascript:alert(1)"` | kept, but rewritten to `unsafe:javascript:alert(1)` | The key word is **allow-list**: the sanitizer does not hunt for known-bad patterns, it keeps only what it knows to be harmless. That is why benign-looking attributes such as `style` disappear too. ## Development versus production - In **development mode**, whenever the sanitizer changes a value Angular logs `WARNING: sanitizing HTML stripped some content` to the console, with a link to the security guide. - In a **production build** the same content is stripped **silently**; no error is thrown and the rest of the markup still renders. So a missing attribute or image in production is often a sanitizer at work, and the place to confirm it is a development build's console. ## Why sanitize rather than reject Angular could have refused any value containing markup it dislikes. It does not, because most real content is mostly benign: a help article with one stray `<style>` block should still show its paragraphs. Sanitizing keeps the useful structure and removes only what can execute or load. The flip side is that the result can differ from the input without any error, so a developer who expects a byte-for-byte copy is surprised. The sanitizer also re-encodes some characters when it serializes the tree, which is why the warning is tied to content actually being stripped rather than to any change at all. ## Where the protection ends Sanitization is a property of **Angular's template bindings** (and of host bindings, which the compiler treats the same way). It does not apply when code writes to the DOM directly through `ElementRef.nativeElement`, `Renderer2.setProperty`, `document` APIs or a third-party library. It also stops applying when someone wraps the value with `DomSanitizer.bypassSecurityTrustHtml`, which tells Angular the value was already vetted. Two practical rules follow for a junior developer: - Use interpolation for user text; reach for `[innerHTML]` only when the markup itself is the content. - Treat a stripped attribute as the sanitizer doing its job, not as a bug to switch off.

  • Is interpolation inside an attribute, such as href="{{ url }}", treated like plain text too?
    It is compiled as a property binding, so the security schema still applies. `title="{{ x }}"` is harmless text, but `href="{{ url }}"` on an anchor is in the URL context and goes through the URL sanitizer exactly like `[href]="url"`, so a `javascript:` value gets the `unsafe:` prefix.
  • Why does Angular sanitize [innerHTML] instead of escaping it like interpolation?
    Because the whole point of binding `innerHTML` is to render markup. Escaping would turn formatting into visible tags. Sanitizing keeps the benign structure, such as bold text, lists and links, and removes only what can execute: scripts, event handlers and `javascript:` URLs.

saying these in an interview costs you the question

  • Interpolation with {{ }} can run a script tag hidden in the string.
  • [innerHTML] inserts the string into the DOM exactly as given.
  • Angular escapes [innerHTML] content, so the tags show up as text.
  • The sanitizer throws an error whenever it meets a script tag.
  • Angular also sanitizes values written through nativeElement.innerHTML.
open as a page

In Angular, what does DomSanitizer.bypassSecurityTrustResourceUrl return, and when is calling one of the bypassSecurityTrust methods justified?

level: middleimportance: must knowfreq 57%

basics

~20 s

It returns a SafeResourceUrl: a wrapper around the unchanged string, branded for the resource URL context, that bindings unwrap without sanitizing. A bypass is justified only for a value you built from trusted parts that Angular would otherwise block.

open as a page

Angular sanitizes bindings per security context; what does it do with an untrusted string bound to [innerHTML], [style], an anchor's [href] and an iframe's [src]?

level: middleimportance: should knowfreq 46%

basics

~20 s

Each binding gets a security context at compile time. HTML is sanitized, style passes through unchanged, a javascript: URL gets an unsafe: prefix while other URLs pass, and a plain string bound to an iframe src throws.

open as a page

A security review flags an Angular directive that writes an input into el.nativeElement.innerHTML; why is that exploitable when [innerHTML] was not, and how do you fix it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Angular's sanitizer runs inside compiled template and host bindings; ElementRef.nativeElement is the raw DOM node, so assigning innerHTML goes straight to the browser. Fix it with a host [innerHTML] binding, or run DomSanitizer.sanitize(SecurityContext.HTML, value) before writing.

open as a page

Your Angular app renders user-authored rich-text comments through [innerHTML] and their inline styles vanish; a teammate proposes bypassSecurityTrustHtml. What goes wrong, and what do you do instead?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Bypassing switches off sanitization for attacker-written markup, so one saved comment with an onerror handler runs in every reader's browser: stored XSS. Keep Angular's sanitizer and carry formatting as classes, or bypass only right after a dedicated sanitizer.

open as a page