Angular part d’une règle simple : toute valeur liée dans un template est considérée comme non sûre, jusqu’à preuve du contraire. L’interpolation échappe le texte. [innerHTML] passe par le sanitizer. Les URL avec des schémas dangereux sont neutralisées. C’est pourquoi une XSS dans une application Angular apparaît presque toujours quand quelqu’un contourne ces protections, pas quand il les utilise.
Cette page montre ce qu’Angular fait automatiquement, où se trouvent les « portes dérobées » à chercher lors de la revue de code, et comment ajouter la deuxième ligne de défense : la Content Security Policy et les Trusted Types. À la fin, la protection XSRF de HttpClient.
localStorage. Les protections par défaut couvrent les cas courants. Le reste dépend de ce que vous faites avec bypassSecurityTrust* et avec le DOM.Contextes de sécurité
Angular choisit le traitement selon l’endroit où la valeur atterrit. Dans du texte, il l’échappe. Dans du HTML ([innerHTML]), il la nettoie : il garde les balises de mise en forme et supprime <script>, les attributs on*, <iframe> et les URL dangereuses. Dans une URL ([href], [src]), il neutralise les schémas comme javascript:. L’URL de ressource (la source d’un <iframe> ou d’un script) est le contexte le plus strict, car là une URL signifie du code qui sera exécuté.
<!-- Interpolation : le texte est ÉCHAPPÉ — « <b> » s’affiche tel quel, il ne devient pas une balise -->
<p>{{ comment.text }}</p>
<!-- [innerHTML] : le HTML est NETTOYÉ — les balises de mise en forme restent, les scripts et les gestionnaires d’événements sont supprimés -->
<div [innerHTML]="comment.html"></div>
<!-- URL : javascript: et les autres schémas dangereux deviennent « unsafe:… » -->
<a [href]="profile.website">site</a>
<!-- Styles : les valeurs de [style] sont aussi vérifiées -->
<div [style.background-image]="'url(' + user.avatar + ')'"></div>[innerHTML] (nettoyé automatiquement) Bonjour, monde ! Voir la doc.
sanitizer.sanitize(SecurityContext.HTML, …)<p><b>Bonjour</b>, <em>monde</em> ! Voir la <a href="https://angular.dev">doc</a>.</p>
rien de supprimé le contenu était déjà sûr.
Avec bypassSecurityTrustHtml(…), Angular insérerait exactement le texte de la zone, gestionnaires d’événements compris. C’est pourquoi la démo ne le fait pas.
URL
Un lien dont l’href vient de l’utilisateur (« mon site web ») est un vecteur d’attaque classique : javascript:alert(document.cookie) exécute du code au clic. Angular préfixe les valeurs non sûres par unsafe:, de sorte que le navigateur ne peut plus les exécuter. Les schémas courants (https:, mailto:, chemins relatifs) passent sans changement.
Template: <a [href]="url()"> → l’attribut dans le DOM :
bypassSecurityTrust et les portes dérobées
DomSanitizer a cinq méthodes bypassSecurityTrust… (Html, Script, Style, Url, ResourceUrl). Elles marquent une valeur comme sûre, et Angular ne la vérifie plus. Elles ne sont légitimes que pour du contenu que vous construisez et contrôlez vous-même, par exemple l’URL d’un embed construite à partir d’un id validé. Sur des données d’utilisateurs ou d’une API externe, chaque appel est une vulnérabilité. La documentation d’Angular les qualifie de « sensibles pour la sécurité » : cherchez-les à chaque revue.
import { DomSanitizer, SafeHtml, SafeResourceUrl } from '@angular/platform-browser';
export class Embed {
private readonly sanitizer = inject(DomSanitizer);
// ✓ Uniquement pour du contenu que vous CONTRÔLEZ : une URL que vous construisez vous-même à partir d’un id validé
readonly video: SafeResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
`https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId())}`,
);
// ✗ Jamais sur une entrée qui vient de l’utilisateur ou d’une API externe
unsafe(html: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(html); // XSS garantie si html contient un <img onerror>
}
}Plus dangereux encore, les chemins qui ne passent pas du tout par Angular : nativeElement.innerHTML, Renderer2.setProperty(…, 'innerHTML', …), document.write, eval et les templates construits à partir de chaînes à l’exécution.
// Tout cela CONTOURNE la sanitization d’Angular — évitez-le avec des données venues de l’extérieur :
this.el.nativeElement.innerHTML = userHtml; // DOM direct
this.renderer.setProperty(this.el.nativeElement, 'innerHTML', userHtml);
document.write(userHtml);
new Function(userCode)(); eval(userCode);
// et les templates construits à partir de chaînes contenant des saisies utilisateur (compilation JIT à l’exécution)
// ✓ L’alternative : [innerHTML] (nettoyé) ou, pour du markdown, une bibliothèque de rendu + sanitizationContent Security Policy
La CSP est un en-tête qui indique au navigateur quels scripts et styles ont le droit de s’exécuter. Même si un attaquant parvient à injecter du HTML, son script ne s’exécute pas sans le bon nonce ou hash. Angular ajoute des styles inline (les styles des composants), il lui faut donc un nonce. Il l’obtient soit par l’attribut ngCspNonce sur l’élément racine (le serveur le génère à chaque réponse), soit par le token CSP_NONCE.
L’alternative sans serveur qui génère la page à partir d’un template est security.autoCsp. Au moment du build, la CLI calcule les hashes des scripts inline et place la politique dans une balise <meta>, de sorte que index.html reste cacheable. La documentation en liste les limites : elle ne couvre que les scripts, certaines directives (par exemple frame-ancestors) doivent toujours être envoyées dans l’en-tête, et elle ne fonctionne pas avec le SSR.
import { CSP_NONCE } from '@angular/core';
// Option 1 (templating côté serveur) : le serveur place le même nonce dans l’en-tête et dans index.html
// <app-root ngCspNonce="r4nd0m-per-request"></app-root>
// Option 2 : le nonce est disponible à l’exécution et vous voulez pouvoir mettre index.html en cache
bootstrapApplication(App, {
providers: [{ provide: CSP_NONCE, useValue: globalThis.myRandomNonceValue }],
});Trusted Types
Les Trusted Types vont un cran plus loin : le navigateur refuse toute écriture dans les « sinks » dangereux (innerHTML, le src d’un script…) qui ne passe pas par une politique approuvée. Angular a ses propres politiques : angular pour ce que fait le framework, angular#unsafe-bypass si vous utilisez bypassSecurityTrust*, angular#bundler pour les chunks lazy de la CLI. Un innerHTML écrit directement dans le code devient une erreur plutôt qu’une vulnérabilité.
# Le minimum pour une application Angular
Content-Security-Policy: trusted-types angular; require-trusted-types-for 'script';
# Si vous utilisez bypassSecurityTrust*
Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for 'script';
# Si vous avez des chunks lazy créés par la CLI
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 : le serveur remplace la valeur à chaque réponse -->
<app-root ngCspNonce="RANDOM_PER_RESPONSE"></app-root>
// ou, si le nonce est connu à l’exécution :
{ provide: CSP_NONCE, useValue: globalThis.myNonce }XSRF / CSRF
Si l’authentification repose sur un cookie de session, une page tierce peut envoyer un formulaire à votre API, et le navigateur joint automatiquement le cookie. HttpClient intègre une protection : il lit le cookie XSRF-TOKEN et envoie la valeur dans l’en-tête X-XSRF-TOKEN. Seul le code de votre domaine peut lire le cookie, le serveur sait donc d’où vient la requête. L’en-tête n’est ajouté qu’aux requêtes qui modifient des données (pas à GET/HEAD), vers des URL relatives ou de la même origine. Le côté serveur est obligatoire : le serveur définit le cookie et vérifie l’en-tête.
import { provideHttpClient, withXsrfConfiguration, withNoXsrfProtection } from '@angular/common/http';
// Par défaut : HttpClient lit le cookie XSRF-TOKEN et envoie l’en-tête X-XSRF-TOKEN
// sur les requêtes qui MODIFIENT des données (POST, PUT, DELETE…) vers des URL relatives / la même origine.
provideHttpClient(
withXsrfConfiguration({ cookieName: 'CSRF-TOKEN', headerName: 'X-CSRF-TOKEN' }), // si le backend utilise d’autres noms
);
// Le désactiver (p. ex. une API avec un Bearer token, sans cookie de session) :
provideHttpClient(withNoXsrfProtection());Quand OUI, quand NON
- Laissez Angular faire le binding : interpolation et
[innerHTML], pas le DOM directement. - Une CSP stricte (nonce ou
autoCsp) et les Trusted Types en production. - Des cookies de session
HttpOnly,Secure,SameSite, et la protection XSRF activée (voir Authentification). Pour une connexion via un fournisseur externe, voir OAuth 2.1 / OIDC avec PKCE. - Mettez à jour Angular et les dépendances : les correctifs de sécurité arrivent dans les versions patch.
bypassSecurityTrustHtmlsur du contenu venant d’utilisateurs ou de CMS externes.- Une validation « de sécurité » uniquement dans le frontend : chaque règle doit être revérifiée côté serveur.
- Des access tokens dans
localStoragesans CSP : une seule XSS peut les lire. - Désactiver les avertissements de sanitization « pour qu’ils ne s’affichent plus dans la console » : ils vous montrent exactement où du HTML non sûr entre.
Pièges fréquents
- « La pipe
safeHtml» du projet, qui appellebypassSecurityTrustHtmlsur n’importe quoi : c’est une XSS réutilisable. Supprimez-la ou limitez-la à des sources sûres. - Du markdown rendu avec
bypassSecurityTrustHtml: le markdown peut contenir du HTML. Rendez-le, puis passez le résultat par la sanitization. - Des attributs
hrefconstruits par concaténation dansnativeElement: ils contournent la sanitization des URL. - Une CSP ajoutée « à la fin » sans test : les chunks lazy ou les styles inline sont bloqués et l’application casse en production. Commencez par
Content-Security-Policy-Report-Only. - XSRF désactivé alors qu’un cookie de session est actif : n’importe quel site peut faire des requêtes POST au nom de l’utilisateur.
Aide-mémoire
IMPLICITE (rien à faire)
{{ x }} échappé
[innerHTML] nettoyé (script, on*, javascript:, iframe… supprimés) — avertissement en dev quand il supprime quelque chose
[href] / [src] URL dangereuses → "unsafe:…"
CONTOURNEMENTS (uniquement pour du contenu que vous contrôlez — à chercher en revue de code)
bypassSecurityTrustHtml / Url / ResourceUrl / Style / Script
nativeElement.innerHTML, Renderer2.setProperty('innerHTML'), document.write, eval, templates construits à partir de chaînes
CSP
ngCspNonce="…" on the root or { provide: CSP_NONCE, useValue } — un nonce par réponse
"security": { "autoCsp": true } — hashes au build, index.html cacheable, PAS avec le SSR, scripts uniquement
TRUSTED TYPES
trusted-types angular [angular#unsafe-bypass] [angular#bundler]; require-trusted-types-for 'script'
XSRF
XSRF-TOKEN → X-XSRF-TOKEN, uniquement les requêtes qui modifient, uniquement la même origine · withXsrfConfiguration · withNoXsrfProtection
SERVER
tout revalider · HttpOnly + Secure + SameSite cookies · npm audit / mises à jour d’Angular