Angular geht von einer einfachen Regel aus: Jeder Wert, der im Template gebunden wird, gilt als unsicher, bis das Gegenteil bewiesen ist. Die Interpolation maskiert den Text. [innerHTML] läuft durch den Sanitizer. URLs mit gefährlichen Schemata werden entschärft. Deshalb entsteht ein XSS in einer Angular-Anwendung fast immer dann, wenn jemand diese Schutzmechanismen umgeht, nicht wenn er sie nutzt.
Diese Seite zeigt, was Angular automatisch erledigt, wo die „Seitentüren“ liegen, nach denen du im Code-Review suchen solltest, und wie du die zweite Verteidigungslinie ergänzt: Content Security Policy und Trusted Types. Am Ende steht der XSRF-Schutz in HttpClient.
localStorage. Die Standardschutzmechanismen decken die üblichen Fälle ab. Der Rest hängt davon ab, was du mit bypassSecurityTrust* und dem DOM machst.Security-Kontexte
Angular wählt die Behandlung danach, wo der Wert landet. In Text maskiert es ihn. In HTML ([innerHTML]) bereinigt es ihn: Die Formatierungs-Tags bleiben, <script>, die on*-Attribute, <iframe> und gefährliche URLs werden entfernt. Bei einer URL ([href], [src]) entschärft es Schemata wie javascript:. Die Resource-URL (die Quelle eines <iframe> oder eines Skripts) ist der strengste Kontext, denn dort bedeutet eine URL Code, der ausgeführt wird.
<!-- Interpolation: Der Text wird MASKIERT — „<b>“ erscheint wörtlich, es wird kein Tag -->
<p>{{ comment.text }}</p>
<!-- [innerHTML]: Das HTML wird BEREINIGT — Formatierungs-Tags bleiben, Skripte und Event-Handler werden entfernt -->
<div [innerHTML]="comment.html"></div>
<!-- URLs: javascript: und andere gefährliche Schemata werden zu „unsafe:…“ -->
<a [href]="profile.website">site</a>
<!-- Styles: Auch Werte in [style] werden geprüft -->
<div [style.background-image]="'url(' + user.avatar + ')'"></div>[innerHTML] (automatisch bereinigt) Hallo, Welt! Siehe Doku.
sanitizer.sanitize(SecurityContext.HTML, …)<p><b>Hallo</b>, <em>Welt</em>! Siehe <a href="https://angular.dev">Doku</a>.</p>
nichts entfernt der Inhalt war bereits sicher.
Mit bypassSecurityTrustHtml(…) würde Angular genau den Text aus dem Feld einfügen, einschließlich Event-Handlern. Deshalb macht die Demo das nicht.
URLs
Ein Link mit einem href, der vom Nutzer stammt („meine Website“), ist ein klassischer Angriffsvektor: javascript:alert(document.cookie) führt beim Klick Code aus. Angular stellt unsicheren Werten unsafe: voran, sodass der Browser sie nicht mehr ausführen kann. Gängige Schemata (https:, mailto:, relative Pfade) passieren unverändert.
Template: <a [href]="url()"> → das Attribut im DOM:
bypassSecurityTrust und die Seitentüren
DomSanitizer hat fünf bypassSecurityTrust…-Methoden (Html, Script, Style, Url, ResourceUrl). Sie markieren einen Wert als sicher, und Angular prüft ihn nicht mehr. Legitim sind sie nur für Inhalte, die du selbst erzeugst und kontrollierst, zum Beispiel die URL eines Embeds, die aus einer validierten ID gebaut wird. Bei Daten von Nutzern oder aus einer externen API ist jeder Aufruf eine Schwachstelle. Die Angular-Dokumentation kennzeichnet sie als „security sensitive“: Such bei jedem Review danach.
import { DomSanitizer, SafeHtml, SafeResourceUrl } from '@angular/platform-browser';
export class Embed {
private readonly sanitizer = inject(DomSanitizer);
// ✓ Nur für Inhalte, die du KONTROLLIERST: eine URL, die du selbst aus einer validierten ID baust
readonly video: SafeResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
`https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId())}`,
);
// ✗ Niemals bei Eingaben, die vom Nutzer oder aus einer externen API stammen
unsafe(html: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(html); // garantiertes XSS, wenn html ein <img onerror> enthält
}
}Gefährlicher sind die Wege, die gar nicht über Angular laufen: nativeElement.innerHTML, Renderer2.setProperty(…, 'innerHTML', …), document.write, eval und Templates, die zur Laufzeit aus Strings gebaut werden.
// All das UMGEHT das Sanitizing von Angular — vermeide es bei Daten von außen:
this.el.nativeElement.innerHTML = userHtml; // direktes DOM
this.renderer.setProperty(this.el.nativeElement, 'innerHTML', userHtml);
document.write(userHtml);
new Function(userCode)(); eval(userCode);
// und Templates, die aus Strings mit Nutzereingaben gebaut werden (JIT-Kompilierung zur Laufzeit)
// ✓ Die Alternative: [innerHTML] (bereinigt) oder, für Markdown, eine Rendering-Bibliothek + SanitizingContent Security Policy
CSP ist ein Header, der dem Browser sagt, welche Skripte und Styles ausgeführt werden dürfen. Selbst wenn ein Angreifer HTML einschleusen kann, läuft sein Skript ohne die richtige Nonce oder den richtigen Hash nicht. Angular fügt Inline-Styles hinzu (die Styles der Komponenten) und braucht deshalb eine Nonce. Es bekommt sie entweder über das Attribut ngCspNonce am Root-Element (der Server erzeugt sie bei jeder Antwort neu) oder über das Token CSP_NONCE.
Die Alternative ohne Server, der die Seite per Templating befüllt, ist security.autoCsp. Beim Build berechnet die CLI die Hashes der Inline-Skripte und setzt die Policy in ein <meta>-Tag, sodass index.html cachebar bleibt. Die Dokumentation nennt die Grenzen: Es deckt nur Skripte ab, einige Direktiven (zum Beispiel frame-ancestors) müssen weiterhin im Header gesendet werden, und es funktioniert nicht mit SSR.
import { CSP_NONCE } from '@angular/core';
// Variante 1 (Server-Side-Templating): Der Server setzt dieselbe Nonce in den Header und in index.html
// <app-root ngCspNonce="r4nd0m-per-request"></app-root>
// Variante 2: Die Nonce ist zur Laufzeit verfügbar und du willst index.html cachen können
bootstrapApplication(App, {
providers: [{ provide: CSP_NONCE, useValue: globalThis.myRandomNonceValue }],
});Trusted Types
Trusted Types gehen noch einen Schritt weiter: Der Browser verweigert jeden Schreibzugriff auf gefährliche „Sinks“ (innerHTML, das src eines Skripts …), der nicht über eine freigegebene Policy läuft. Angular bringt eigene Policies mit: angular für das, was das Framework tut, angular#unsafe-bypass, wenn du bypassSecurityTrust* verwendest, und angular#bundler für die Lazy-Chunks der CLI. Ein direkt im Code geschriebenes innerHTML wird so zu einem Fehler statt zu einer Schwachstelle.
# Das Minimum für eine Angular-Anwendung
Content-Security-Policy: trusted-types angular; require-trusted-types-for 'script';
# Wenn du bypassSecurityTrust* verwendest
Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for 'script';
# Wenn du Lazy-Chunks hast, die von der CLI erzeugt werden
Content-Security-Policy: trusted-types angular angular#bundler; require-trusted-types-for 'script';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';
<!-- index.html: der Server ersetzt den Wert bei jeder Antwort -->
<app-root ngCspNonce="RANDOM_PER_RESPONSE"></app-root>
// oder, wenn die Nonce zur Laufzeit bekannt ist:
{ provide: CSP_NONCE, useValue: globalThis.myNonce }XSRF / CSRF
Erfolgt die Authentifizierung über ein Session-Cookie, kann eine fremde Seite ein Formular an deine API senden, und der Browser hängt das Cookie automatisch an. HttpClient hat einen eingebauten Schutz: Es liest das Cookie XSRF-TOKEN und sendet den Wert im Header X-XSRF-TOKEN. Nur Code auf deiner Domain kann das Cookie lesen, daher weiß der Server, woher der Request kommt. Der Header wird nur bei Requests hinzugefügt, die Daten verändern (nicht bei GET/HEAD), und nur bei relativen URLs oder derselben Origin. Die Serverseite ist Pflicht: Der Server setzt das Cookie und prüft den Header.
import { provideHttpClient, withXsrfConfiguration, withNoXsrfProtection } from '@angular/common/http';
// Standard: HttpClient liest das Cookie XSRF-TOKEN und sendet den Header X-XSRF-TOKEN
// bei Requests, die Daten VERÄNDERN (POST, PUT, DELETE…), an relative URLs / dieselbe Origin.
provideHttpClient(
withXsrfConfiguration({ cookieName: 'CSRF-TOKEN', headerName: 'X-CSRF-TOKEN' }), // wenn das Backend andere Namen verwendet
);
// Deaktivieren (z. B. bei einer API mit Bearer-Token, ohne Session-Cookie):
provideHttpClient(withNoXsrfProtection());Wann ja, wann nein
- Lass Angular das Binding übernehmen: Interpolation und
[innerHTML], nicht direkt das DOM. - Eine strikte CSP (Nonce oder
autoCsp) und Trusted Types in Produktion. - Session-Cookies mit
HttpOnly,Secure,SameSiteund aktivem XSRF-Schutz (siehe Authentifizierung). Für den Login über einen externen Anbieter siehe OAuth 2.1 / OIDC mit PKCE. - Halte Angular und die Abhängigkeiten aktuell: Sicherheitskorrekturen kommen in Patch-Versionen.
bypassSecurityTrustHtmlbei Inhalten von Nutzern oder aus externen CMS.- „Sicherheits“-Validierung nur im Frontend: Jede Regel muss auf dem Server erneut geprüft werden.
- Access-Tokens in
localStorageohne CSP: Ein einziges XSS kann sie lesen. - Die Sanitizing-Warnungen abschalten, „damit sie nicht mehr in der Konsole erscheinen“: Sie zeigen dir genau, wo unsicheres HTML hineingelangt.
Häufige Stolperfallen
- Die „
safeHtml-Pipe“ im Projekt, die auf allesbypassSecurityTrustHtmlaufruft: Sie ist ein wiederverwendbares XSS. Lösche sie oder beschränke sie auf sichere Quellen. - Markdown, gerendert mit
bypassSecurityTrustHtml: Markdown kann HTML enthalten. Rendere es, und schicke das Ergebnis dann durch das Sanitizing. href-Attribute, die per Konkatenation gebaut werden innativeElement: Sie umgehen das URL-Sanitizing.- CSP, die „am Ende“ ungetestet ergänzt wird: Lazy-Chunks oder Inline-Styles werden blockiert, und die Anwendung geht in Produktion kaputt. Beginne mit
Content-Security-Policy-Report-Only. - XSRF deaktiviert, während ein Session-Cookie aktiv ist: Jede Website kann POST-Requests im Namen des Nutzers senden.
Spickzettel
IMPLIZIT (nichts zu tun)
{{ x }} maskiert
[innerHTML] bereinigt (script, on*, javascript:, iframe… werden entfernt) — Dev-Warnung, wenn etwas entfernt wird
[href] / [src] gefährliche URLs → "unsafe:…"
UMGEHUNGEN (nur für Inhalte, die du kontrollierst — beim Code-Review danach suchen)
bypassSecurityTrustHtml / Url / ResourceUrl / Style / Script
nativeElement.innerHTML, Renderer2.setProperty('innerHTML'), document.write, eval, aus Strings gebaute Templates
CSP
ngCspNonce="…" am Root oder { provide: CSP_NONCE, useValue } — Nonce pro Antwort
"security": { "autoCsp": true } — Hashes beim Build, index.html cachebar, NICHT mit SSR, nur script-src
TRUSTED TYPES
trusted-types angular [angular#unsafe-bypass] [angular#bundler]; require-trusted-types-for 'script'
XSRF
XSRF-TOKEN → X-XSRF-TOKEN, nur Requests, die verändern, nur Same-Origin · withXsrfConfiguration · withNoXsrfProtection
SERVER
alles erneut validieren · HttpOnly-, Secure- und SameSite-Cookies · npm audit / Angular-Updates