skip to content

Web Attack Defenses

Angular's built-in defences: sanitization of bound values per security context, Trusted Types and CSP nonce support, and HttpClient's XSRF header and XSSI prefix. Interviewers probe where each stops.

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

explore

questions

13

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

Why can an Angular app compiled ahead of time run under a CSP without 'unsafe-eval', while a JIT-compiled app cannot?

level: juniorimportance: should knowfreq 38%

basics

~20 s

AOT compiles templates into JavaScript at build time, so the browser only runs ordinary bundled code. The JIT compiler generates template code in the browser and evaluates it with eval or new Function, which a script-src without 'unsafe-eval' blocks.

open as a page

In an Angular app, how do you change the XSRF cookie and header names HttpClient uses, or turn its XSRF handling off, and when is each justified?

level: juniorimportance: should knowfreq 30%

basics

~20 s

Pass withXsrfConfiguration({cookieName, headerName}) to provideHttpClient to match your backend's names, or withNoXsrfProtection() to remove the interceptor. Rename when the backend or a shared domain needs it; disable only when another defence, such as header-sent bearer tokens, makes it unnecessary.

open as a page

Under a nonce-based CSP, why do an Angular app's component styles need a nonce, and when do you use CSP_NONCE versus the ngCspNonce attribute?

level: middleimportance: should knowfreq 34%

basics

~20 s

Angular inserts component styles as <style> elements at runtime, which a strict style-src blocks unless they carry the nonce. Use ngCspNonce when the server templates index.html per response, and CSP_NONCE when the nonce arrives at runtime and index.html stays cacheable.

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

You are deploying a strict nonce-based CSP for an Angular banking dashboard; what does Angular itself require in the policy, and which app features would force you to loosen it?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Angular's minimal policy is default-src 'self' plus a per-response nonce in style-src and script-src, handed to Angular via ngCspNonce or CSP_NONCE. JIT compilation, JSONP, bypassed values under Trusted Types and missing nonce plumbing are what push teams to loosen it.

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

After an Angular app's services switch to calling https://api.example.com directly, every POST fails the backend's CSRF check with a 403; why did HttpClient stop sending X-XSRF-TOKEN, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

HttpClient adds the XSRF header only for URLs on the page's own origin; api.example.com is cross-origin, so it is skipped to avoid leaking the token. Serve the API from the app's origin, or add the header for that one trusted origin.

open as a page

What is the )]}', prefix some servers put in front of JSON responses, and what does Angular's HttpClient do with it?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

It is an XSSI guard: prefixing JSON with )]}' and a newline makes the response a syntax error when a hostile page loads it as a script. HttpClient strips it before parsing JSON, so application code never sees it.

open as a page

When you enforce Trusted Types on an Angular app, which Angular policy names must the trusted-types directive allow, and what breaks if one is missing?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

The angular policy is always required; angular#unsafe-bypass is needed for bypassSecurityTrust methods, angular#bundler for CLI lazy chunks, angular#unsafe-jit for JIT and angular#unsafe-upgrade for AngularJS hybrids. Without one, that feature's DOM writes fail as violations.

open as a page