AngularAtlased. 22.2

Ch. 17Security & globalisation72 / 75

Security: XSS, sanitisation & CSP

What Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.

~0 min read Official docs

Angular starts from one simple rule: every value bound in a template is considered unsafe, until proven otherwise. Interpolation escapes the text. [innerHTML] goes through the sanitizer. URLs with dangerous schemes are neutralized. That is why an XSS in an Angular application almost always appears when someone bypasses these protections, not when they use them.

This page shows what Angular does automatically, where the “side doors” are that you should look for at code review, and how to add the second line of defence: Content Security Policy and Trusted Types. At the end is the XSRF protection in HttpClient.

Why it matters
A single XSS means the attacker runs JavaScript on your page, with the user’s session: they read data, send requests on their behalf, steal tokens from localStorage. The default protections cover the common cases. The rest depends on what you do with bypassSecurityTrust* and with the DOM.

Security contexts

Angular chooses the treatment according to where the value ends up. In text it escapes it. In HTML ([innerHTML]) it sanitizes it: it keeps the formatting tags and removes <script>, the on* attributes, <iframe> and dangerous URLs. In a URL ([href], [src]) it neutralizes schemes such as javascript:. Resource URL (the source of an <iframe> or of a script) is the strictest context, because there a URL means code that gets executed.

<!-- Interpolation: the text is ESCAPED — “<b>” shows up literally, it does not become a tag -->
<p>{{ comment.text }}</p>

<!-- [innerHTML]: the HTML is SANITIZED — formatting tags stay, script and event handlers are removed -->
<div [innerHTML]="comment.html"></div>

<!-- URLs: javascript: and other dangerous schemes become “unsafe:…” -->
<a [href]="profile.website">site</a>

<!-- Styles: values in [style] are checked too -->
<div [style.background-image]="'url(' + user.avatar + ')'"></div>
LiveAngular’s sanitizer, live
Rendered with [innerHTML] (sanitized automatically)

Hello, world! See docs.

What remains after sanitizer.sanitize(SecurityContext.HTML, …)
<p><b>Hello</b>, <em>world</em>! See <a href="https://angular.dev">docs</a>.</p>

nothing removed the content was already safe.

With bypassSecurityTrustHtml(…) Angular would insert exactly the text in the box, including event handlers. That is why the demo does not do this.

Pick a dangerous example: on the left you see what [innerHTML] renders, on the right exactly what the sanitizer let through, and below it what was removed. In development mode, Angular writes a warning to the console every time it removes something; that is normal.

URLs

A link with an href that comes from the user (“my website”) is a classic attack vector: javascript:alert(document.cookie) runs code on click. Angular prefixes unsafe values with unsafe:, so the browser can no longer execute them. Common schemes (https:, mailto:, relative paths) pass through unchanged.

LiveWhat [href] receives

Template: <a [href]="url()"> → the attribute in the DOM:

the rendered link
Try javascript: and data:. The attribute in the DOM gets the unsafe: prefix. The click no longer executes anything.

bypassSecurityTrust and the side doors

DomSanitizer has five bypassSecurityTrust… methods (Html, Script, Style, Url, ResourceUrl). They mark a value as safe, and Angular no longer checks it. They are legitimate only for content that you build and control yourself, for example the URL of an embed built from a validated id. On data from users or from an external API, every call is a vulnerability. The Angular documentation marks them as “security sensitive”: look for them in every review.

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

export class Embed {
  private readonly sanitizer = inject(DomSanitizer);

  // ✓ Only for content you CONTROL: a URL you build yourself from a validated id
  readonly video: SafeResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
    `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId())}`,
  );

  // ✗ Never on input that comes from the user or from an external API
  unsafe(html: string): SafeHtml {
    return this.sanitizer.bypassSecurityTrustHtml(html); // guaranteed XSS if html contains an <img onerror>
  }
}

More dangerous are the paths that do not go through Angular at all: nativeElement.innerHTML, Renderer2.setProperty(…, 'innerHTML', …), document.write, eval and templates built from strings at runtime.

// All of these BYPASS Angular’s sanitization — avoid them with data from outside:
this.el.nativeElement.innerHTML = userHtml;                     // direct DOM
this.renderer.setProperty(this.el.nativeElement, 'innerHTML', userHtml);
document.write(userHtml);
new Function(userCode)();  eval(userCode);
// and templates built from strings with user input (JIT compilation at runtime)

// ✓ The alternative: [innerHTML] (sanitized) or, for markdown, a rendering library + sanitization

Content Security Policy

CSP is a header that tells the browser which scripts and styles are allowed to run. Even if an attacker manages to inject HTML, their script does not run without the correct nonce or hash. Angular adds inline styles (the component styles), so it needs a nonce. It gets one either through the ngCspNonce attribute on the root element (the server generates it on every response), or through the CSP_NONCE token.

The alternative without a server that templates the page is security.autoCsp. At build time, the CLI computes the hashes of the inline scripts and puts the policy in a <meta> tag, so index.html stays cacheable. The documentation lists its limits: it covers only scripts, some directives (for example frame-ancestors) still have to be sent in the header, and it does not work with SSR.

import { CSP_NONCE } from '@angular/core';

// Option 1 (server-side templating): the server puts the same nonce in the header and in index.html
//   <app-root ngCspNonce="r4nd0m-per-request"></app-root>

// Option 2: the nonce is available at runtime and you want to be able to cache index.html
bootstrapApplication(App, {
  providers: [{ provide: CSP_NONCE, useValue: globalThis.myRandomNonceValue }],
});

Trusted Types

Trusted Types go one step further: the browser refuses any write to dangerous “sinks” (innerHTML, the src of a script…) that does not come through an approved policy. Angular has its own policies: angular for what the framework does, angular#unsafe-bypass if you use bypassSecurityTrust*, angular#bundler for the CLI’s lazy chunks. An innerHTML written directly in code becomes an error instead of a vulnerability.

# The minimum for an Angular application
Content-Security-Policy: trusted-types angular; require-trusted-types-for 'script';

# If you use bypassSecurityTrust*
Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for 'script';

# If you have lazy chunks created by the CLI
Content-Security-Policy: trusted-types angular angular#bundler; require-trusted-types-for 'script';
LiveCSP builder
HTTP header
Content-Security-Policy:
  script-src 'self' 'nonce-RANDOM_PER_RESPONSE';
  style-src 'self' 'nonce-RANDOM_PER_RESPONSE';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  trusted-types angular angular#bundler;
  require-trusted-types-for 'script';
Configuration in Angular
<!-- index.html: the server replaces the value on every response -->
<app-root ngCspNonce="RANDOM_PER_RESPONSE"></app-root>

// or, if the nonce is known at runtime:
{ provide: CSP_NONCE, useValue: globalThis.myNonce }
Choose how you deliver the nonce and what the application uses. The header and the Angular configuration update on the spot.

XSRF / CSRF

If authentication is done with a session cookie, a foreign page can send a form to your API, and the browser attaches the cookie automatically. HttpClient has built-in protection: it reads the XSRF-TOKEN cookie and sends the value in the X-XSRF-TOKEN header. Only code on your domain can read the cookie, so the server knows where the request comes from. The header is added only to requests that modify data (not to GET/HEAD), to relative URLs or to the same origin. The server side is mandatory: the server sets the cookie and verifies the header.

import { provideHttpClient, withXsrfConfiguration, withNoXsrfProtection } from '@angular/common/http';

// Default: HttpClient reads the XSRF-TOKEN cookie and sends the X-XSRF-TOKEN header
// on requests that MODIFY data (POST, PUT, DELETE…) to relative URLs / the same origin.
provideHttpClient(
  withXsrfConfiguration({ cookieName: 'CSRF-TOKEN', headerName: 'X-CSRF-TOKEN' }), // if the backend uses other names
);

// Disabling it (e.g. an API with a Bearer token, without a session cookie):
provideHttpClient(withNoXsrfProtection());

When YES, when NO

Do this
  • Let Angular do the binding: interpolation and [innerHTML], not the DOM directly.
  • A strict CSP (nonce or autoCsp) and Trusted Types in production.
  • Session cookies that are HttpOnly, Secure, SameSite, and XSRF protection active (see Authentication). For login through an external provider, see OAuth 2.1 / OIDC with PKCE.
  • Update Angular and the dependencies: security fixes arrive in patch versions.
Don’t do this
  • bypassSecurityTrustHtml on content from users or from external CMSs.
  • “Security” validation only in the frontend: every rule must be checked again on the server.
  • Access tokens in localStorage without a CSP: a single XSS can read them.
  • Disabling the sanitization warnings “so they stop appearing in the console”: they show you exactly where unsafe HTML enters.

Common pitfalls

  • “The safeHtml pipe” in the project, which calls bypassSecurityTrustHtml on anything: it is a reusable XSS. Delete it or limit it to safe sources.
  • Markdown rendered with bypassSecurityTrustHtml: markdown can contain HTML. Render it, then pass the result through sanitization.
  • href attributes built by concatenation in nativeElement: they bypass URL sanitization.
  • CSP added “at the end” without testing: lazy chunks or inline styles get blocked and the application breaks in production. Start with Content-Security-Policy-Report-Only.
  • XSRF disabled while a session cookie is active: any site can make POST requests on behalf of the user.

Cheat sheet

IMPLICIT (nothing to do)
  {{ x }}            escaped
  [innerHTML]        sanitized (script, on*, javascript:, iframe… removed) — dev warning when it removes something
  [href] / [src]     dangerous URLs → "unsafe:…"
BYPASSES (only for content you control — look for them at code review)
  bypassSecurityTrustHtml / Url / ResourceUrl / Style / Script
  nativeElement.innerHTML, Renderer2.setProperty('innerHTML'), document.write, eval, templates built from strings
CSP
  ngCspNonce="…" on the root  or  { provide: CSP_NONCE, useValue }  — nonce per response
  "security": { "autoCsp": true }  — hashes at build time, index.html cacheable, NOT with SSR, scripts only
TRUSTED TYPES
  trusted-types angular [angular#unsafe-bypass] [angular#bundler]; require-trusted-types-for 'script'
XSRF
  XSRF-TOKEN → X-XSRF-TOKEN, only requests that modify, only same-origin · withXsrfConfiguration · withNoXsrfProtection
SERVER
  validate everything again · HttpOnly + Secure + SameSite cookies · npm audit / Angular updates

Ch. 17 · 0 of 4 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. Security: XSS, sanitisation & CSP
  2. Authentication: tokens & refresh
  3. OAuth 2.1 & OpenID Connect with PKCE
  4. Internationalisation (i18n)
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation