Cours Angular · Module 12: Sécurité et performances
La sécurité dans Angular - Les fortifications du château
Dans cette leçon5
La sécurité d'une application, c'est comme les murs du château de Himeji 城 : une défense à plusieurs niveaux contre les ennemis. Le château de Himeji a survécu pendant des siècles grâce à ses fortifications bien conçues : ton application a besoin du même niveau de protection !
Types de menaces
| Menace | Description | Protection Angular |
|---|---|---|
| XSS | Injection de scripts malveillants | Assainissement automatique |
| CSRF | Falsification de requête intersite | HttpXsrfModule |
| Injection | Injection de code | Requêtes paramétrées |
| Clickjacking | Iframes cachées | X-Frame-Options |
Protection contre le XSS
Angular assainit automatiquement toutes les valeurs dans les templates :
1@Component({
2 template: `
3 <!-- Sûr - Angular échappe le HTML -->
4 <div>{{ userInput }}</div>
5
6 <!-- Sûr - Angular assainit l'URL -->
7 <a [href]="userUrl">Link</a>
8
9 <!-- Sûr - Angular assainit les styles -->
10 <div [style.background]="userBackground">...</div>
11 `
12})
13export class SafeComponent {
14 // Même si userInput = '<script>alert("XSS")</script>'
15 // Angular affichera le texte, sans exécuter le script
16 userInput = '<script>alert("XSS")</script>';
17}DomSanitizer - Contournement conscient
Il arrive que tu doives afficher du HTML de confiance (par exemple, depuis un éditeur WYSIWYG) :
1import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser';
2
3@Component({
4 template: `
5 <!-- Utilisation d'un pipe ou d'un binding -->
6 <div [innerHTML]="trustedHtml"></div>
7 <iframe [src]="trustedUrl"></iframe>
8 `
9})
10export class TrustedContentComponent {
11 private sanitizer = inject(DomSanitizer);
12
13 trustedHtml: SafeHtml;
14 trustedUrl: SafeUrl;
15
16 constructor() {
17 // IMPORTANT : à utiliser uniquement pour des sources DE CONFIANCE !
18 this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(
19 '<b>Trusted HTML</b>'
20 );
21
22 this.trustedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
23 'https://trusted-site.com/embed'
24 );
25 }
26}
27
28// Pipe réutilisable
29@Pipe({ name: 'safeHtml', standalone: true })
30export class SafeHtmlPipe implements PipeTransform {
31 private sanitizer = inject(DomSanitizer);
32
33 transform(value: string): SafeHtml {
34 return this.sanitizer.bypassSecurityTrustHtml(value);
35 }
36}Content Security Policy (CSP)
La CSP est une couche de défense supplémentaire : comme les murs extérieurs du château.
1<!-- index.html -->
2<meta http-equiv="Content-Security-Policy" content="
3 default-src 'self';
4 script-src 'self' 'unsafe-inline';
5 style-src 'self' 'unsafe-inline';
6 img-src 'self' data: https:;
7 font-src 'self';
8 connect-src 'self' https://api.example.com;
9">Protection contre le CSRF
1// app.config.ts
2import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';
3
4export const appConfig: ApplicationConfig = {
5 providers: [
6 provideHttpClient(
7 withXsrfConfiguration({
8 cookieName: 'XSRF-TOKEN',
9 headerName: 'X-XSRF-TOKEN'
10 })
11 )
12 ]
13};Code de cette leçon : src/app/app.component.ts
1import { Component, signal, computed } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
4
5@Component({
6 selector: 'app-root',
7 standalone: true,
8 imports: [CommonModule],
9 template: `
10 <div class="security-castle">
11 <h1>Fortifications du Château - Sécurité Angular</h1>
12
13 <section class="xss-demo">
14 <h2>Protection XSS</h2>
15
16 <div class="demo-box">
17 <h3>Assainissement automatique d'Angular</h3>
18
19 <div class="input-group">
20 <label>Entrez du texte (essayez <script> ou onclick) :</label>
21 <input
22 type="text"
23 [value]="userInput()"
24 (input)="onInputChange($event)"
25 placeholder="<script>alert('XSS')</script>"
26 >
27 </div>
28
29 <div class="output-comparison">
30 <div class="output safe">
31 <h4>Comportement par défaut d'Angular (sûr) :</h4>
32 <div class="content">{{ userInput() }}</div>
33 <p class="note">Angular échappe automatiquement le HTML</p>
34 </div>
35
36 <div class="output dangerous">
37 <h4>innerHTML sans assainissement :</h4>
38 <div class="content" [innerHTML]="userInput()"></div>
39 <p class="note">Angular assainit automatiquement innerHTML</p>
40 </div>
41
42 <div class="output bypass">
43 <h4>bypassSecurityTrust (soyez prudent !) :</h4>
44 <div class="content" [innerHTML]="trustedHtml()"></div>
45 <p class="note">Uniquement pour des sources fiables !</p>
46 </div>
47 </div>
48 </div>
49 </section>
50
51 <section class="sanitizer-api">
52 <h2>DomSanitizer API</h2>
53
54 <div class="methods-grid">
55 <div class="method">
56 <code>bypassSecurityTrustHtml()</code>
57 <p>HTML de confiance</p>
58 </div>
59 <div class="method">
60 <code>bypassSecurityTrustStyle()</code>
61 <p>Styles CSS de confiance</p>
62 </div>
63 <div class="method">
64 <code>bypassSecurityTrustScript()</code>
65 <p>Script de confiance</p>
66 </div>
67 <div class="method">
68 <code>bypassSecurityTrustUrl()</code>
69 <p>URL de confiance</p>
70 </div>
71 <div class="method">
72 <code>bypassSecurityTrustResourceUrl()</code>
73 <p>URL de ressource de confiance (iframe)</p>
74 </div>
75 </div>
76
77 <div class="warning-box">
78 <h3>Avertissement</h3>
79 <p>N'utilisez jamais bypass* pour des données utilisateur ! Uniquement pour des sources fiables et contrôlées.</p>
80 </div>
81 </section>
82
83 <section class="code-example">
84 <h2>Exemple de code sécurisé</h2>
85 <pre><code>{{ safeCodeExample }}</code></pre>
86 </section>
87
88 <section class="checklist">
89 <h2>Liste de vérification sécurité</h2>
90 <ul>
91 @for (item of securityChecklist(); track item.id) {
92 <li [class.checked]="item.checked" (click)="toggleCheck(item.id)">
93 <span class="checkbox">{{ item.checked ? '[x]' : '[ ]' }}</span>
94 <span class="text">{{ item.text }}</span>
95 </li>
96 }
97 </ul>
98 <p class="progress">
99 Complété : {{ checkedCount() }}/{{ securityChecklist().length }}
100 </p>
101 </section>
102 </div>
103 `,
104 styles: [`
105 .security-castle {
106 font-family: 'Segoe UI', sans-serif;
107 padding: 20px;
108 background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
109 min-height: 100vh;
110 color: #ecf0f1;
111 }
112
113 h1 {
114 text-align: center;
115 color: #e74c3c;
116 }
117
118 h2 {
119 color: #e74c3c;
120 border-bottom: 2px solid #e74c3c;
121 padding-bottom: 10px;
122 }
123
124 section {
125 background: rgba(255,255,255,0.05);
126 border-radius: 10px;
127 padding: 20px;
128 margin: 20px 0;
129 }
130
131 .demo-box {
132 background: rgba(0,0,0,0.3);
133 padding: 20px;
134 border-radius: 8px;
135 }
136
137 .input-group {
138 margin-bottom: 20px;
139 }
140
141 .input-group label {
142 display: block;
143 margin-bottom: 8px;
144 color: #bdc3c7;
145 }
146
147 .input-group input {
148 width: 100%;
149 padding: 12px;
150 border: 2px solid #34495e;
151 border-radius: 5px;
152 background: #2c3e50;
153 color: white;
154 font-size: 16px;
155 box-sizing: border-box;
156 }
157
158 .output-comparison {
159 display: grid;
160 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
161 gap: 15px;
162 }
163
164 .output {
165 padding: 15px;
166 border-radius: 8px;
167 }
168
169 .output.safe {
170 background: rgba(46, 204, 113, 0.2);
171 border: 2px solid #2ecc71;
172 }
173
174 .output.dangerous {
175 background: rgba(241, 196, 15, 0.2);
176 border: 2px solid #f1c40f;
177 }
178
179 .output.bypass {
180 background: rgba(231, 76, 60, 0.2);
181 border: 2px solid #e74c3c;
182 }
183
184 .output h4 {
185 margin-top: 0;
186 font-size: 0.9em;
187 }
188
189 .output .content {
190 background: rgba(0,0,0,0.3);
191 padding: 10px;
192 border-radius: 4px;
193 min-height: 40px;
194 word-break: break-all;
195 }
196
197 .output .note {
198 font-size: 0.8em;
199 color: #888;
200 margin-bottom: 0;
201 }
202
203 .methods-grid {
204 display: grid;
205 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
206 gap: 15px;
207 margin: 20px 0;
208 }
209
210 .method {
211 background: rgba(0,0,0,0.3);
212 padding: 15px;
213 border-radius: 8px;
214 text-align: center;
215 }
216
217 .method code {
218 display: block;
219 background: #e74c3c;
220 color: white;
221 padding: 8px;
222 border-radius: 4px;
223 margin-bottom: 10px;
224 font-size: 0.85em;
225 }
226
227 .method p {
228 margin: 0;
229 color: #bdc3c7;
230 }
231
232 .warning-box {
233 background: rgba(231, 76, 60, 0.2);
234 border: 2px solid #e74c3c;
235 border-radius: 8px;
236 padding: 15px;
237 }
238
239 .warning-box h3 {
240 margin-top: 0;
241 color: #e74c3c;
242 }
243
244 pre {
245 background: #1a252f;
246 padding: 15px;
247 border-radius: 8px;
248 overflow-x: auto;
249 border-left: 4px solid #e74c3c;
250 }
251
252 code {
253 color: #ecf0f1;
254 font-family: 'Fira Code', monospace;
255 font-size: 0.85em;
256 }
257
258 .checklist ul {
259 list-style: none;
260 padding: 0;
261 }
262
263 .checklist li {
264 display: flex;
265 align-items: center;
266 gap: 10px;
267 padding: 12px;
268 margin: 5px 0;
269 background: rgba(0,0,0,0.3);
270 border-radius: 5px;
271 cursor: pointer;
272 transition: all 0.3s;
273 }
274
275 .checklist li:hover {
276 background: rgba(0,0,0,0.5);
277 }
278
279 .checklist li.checked {
280 background: rgba(46, 204, 113, 0.2);
281 }
282
283 .checklist .checkbox {
284 font-size: 1.2em;
285 }
286
287 .progress {
288 text-align: center;
289 font-weight: bold;
290 color: #2ecc71;
291 }
292 `]
293})
294export class AppComponent {
295 userInput = signal('<b>Gras</b> <script>alert("XSS")</script> <img onerror="alert(1)" src="x">');
296
297 securityChecklist = signal([
298 { id: 1, text: 'Ne pas utiliser bypassSecurityTrust* pour des données utilisateur', checked: false },
299 { id: 2, text: 'Valider les données côté serveur', checked: false },
300 { id: 3, text: 'Utiliser des cookies HttpOnly pour les tokens', checked: false },
301 { id: 4, text: 'Mettre en place une CSP (Content Security Policy)', checked: false },
302 { id: 5, text: 'Assainir les URLs avant utilisation', checked: false },
303 { id: 6, text: 'Éviter eval() et innerHTML avec des données utilisateur', checked: false },
304 { id: 7, text: 'Utiliser HTTPS partout', checked: false },
305 { id: 8, text: 'Mettre à jour Angular vers la dernière version', checked: false }
306 ]);
307
308 checkedCount = computed(() =>
309 this.securityChecklist().filter(item => item.checked).length
310 );
311
312 safeCodeExample = `// Utilisation sécurisée de DomSanitizer
313import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
314
315@Component({...})
316export class SafeComponent {
317 private sanitizer = inject(DomSanitizer);
318
319 // Pour du HTML de confiance provenant du serveur (ex. CMS)
320 trustedContent: SafeHtml;
321
322 loadTrustedContent(): void {
323 // Uniquement pour du contenu provenant d'une source fiable !
324 const htmlFromCMS = this.cmsService.getContent();
325 this.trustedContent = this.sanitizer.bypassSecurityTrustHtml(htmlFromCMS);
326 }
327
328 // Ne JAMAIS faire ça avec des données utilisateur !
329 // userInput = this.sanitizer.bypassSecurityTrustHtml(userProvidedData);
330
331 // Pour des URLs dynamiques
332 getVideoUrl(videoId: string): SafeResourceUrl {
333 const url = \`https://youtube.com/embed/\${videoId}\`;
334 return this.sanitizer.bypassSecurityTrustResourceUrl(url);
335 }
336}`;
337
338 constructor(private sanitizer: DomSanitizer) {}
339
340 trustedHtml = computed(() => {
341 // Démonstration - en pratique, à utiliser uniquement pour des sources fiables !
342 return this.sanitizer.bypassSecurityTrustHtml(this.userInput());
343 });
344
345 onInputChange(event: Event): void {
346 const input = event.target as HTMLInputElement;
347 this.userInput.set(input.value);
348 }
349
350 toggleCheck(id: number): void {
351 this.securityChecklist.update(list =>
352 list.map(item =>
353 item.id === id ? { ...item, checked: !item.checked } : item
354 )
355 );
356 }
357}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Comment Angular protège-t-il contre les attaques XSS ?
2. Que signifie l'abréviation XSS dans le contexte de la sécurité des applications web ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète le composant qui affiche du HTML de confiance provenant d'un CMS.
- Tri horizontal
Ordonne la ligne d'import de DomSanitizer dans le bon ordre (de gauche à droite) :
- Clique dans l'ordre
Clique dans l'ordre pour construire l'appel qui renvoie du HTML de confiance :