Cours Angular · Module 12: Sécurité et performances

La sécurité dans Angular - Les fortifications du château

2 min de lecture
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

MenaceDescriptionProtection Angular
XSSInjection de scripts malveillantsAssainissement automatique
CSRFFalsification de requête intersiteHttpXsrfModule
InjectionInjection de codeRequêtes paramétrées
ClickjackingIframes cachéesX-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 &lt;script&gt; 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. 1. Comment Angular protège-t-il contre les attaques XSS ?

  2. 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 :

Articles utiles