Tous ceux qui ont dû intégrer du contenu HTML dans le DOM dans Angular ont vu ce message. Bien sûr, nous obtenons tous le contenu vérifié de notre propre serveur et voulons simplement couvrir le message d'erreur. Ou nous incorporons du HTML à partir de nos constantes, incorporons nos icônes SVG, car nous avons juste besoin de les colorer dans la couleur du texte. Après tout, rien de mauvais ne se produira si nous disons simplement à Angular - pas une dérive, tout est propre là-bas.
Le plus souvent, cela peut être le cas, mais dans les grands projets avec une masse de développeurs qui écrivent des composants indépendants, vous ne savez jamais avec certitude où se trouve votre code. Et si vous, comme moi, développez une bibliothèque de composants réutilisables, alors cette situation doit être résolue dans l'œuf.

Sanitize et DomSanitizer
Angular a des classes abstraites dont les implémentations sont conçues pour effacer le contenu des fichiers indésirables:
abstract class Sanitizer { abstract sanitize(context: SecurityContext, value: string | {}): string | null }
abstract class DomSanitizer implements Sanitizer { abstract sanitize(context: SecurityContext, value: string | SafeValue): string | null abstract bypassSecurityTrustHtml(value: string): SafeHtml abstract bypassSecurityTrustStyle(value: string): SafeStyle abstract bypassSecurityTrustScript(value: string): SafeScript abstract bypassSecurityTrustUrl(value: string): SafeUrl abstract bypassSecurityTrustResourceUrl(value: string): SafeResourceUrl }
La classe interne spécifique que DomSanitizerImpl
Angular utilise dans la construction du DOM. C'est son peuple qui ajoute aux directives, aux composants et aux tuyaux pour dire au framework que ce contenu peut être fiable.
Le code source de cette classe est assez simple:
https://github.com/angular/angular/blob/8.1.0/packages/platform-browser/src/security/dom_sanitization_service.ts#L148
Ce service introduit le concept de SafeValue
. En substance, il s'agit simplement d'une classe wrapper pour une chaîne. Lorsque Renderer
insère une valeur via la liaison, que ce soit @HostBinding
, @HostBinding
, style
ou src
, la valeur est exécutée via la méthode sanitize
. Si SafeValue
est déjà arrivé, il renvoie simplement la chaîne SafeValue
, sinon il nettoie le contenu de lui-même.
Angular n'est pas une bibliothèque spécialisée de nettoyage de codes malveillants. Par conséquent, le cadre suit à juste titre la voie du moindre risque et coupe tout ce qui inquiète. Le code SVG se transforme en lignes vides, les styles en ligne sont supprimés, etc. Cependant, il existe des bibliothèques conçues uniquement pour sécuriser le DOM, dont DOMPurify:
https://github.com/cure53/DOMPurify

Le bon tube SafeHtml
Après avoir connecté DOMPurify, nous pouvons créer un canal qui non seulement marque le contenu comme sûr, mais le nettoie également. Pour ce faire, DOMPurify.sanitize
valeur d'entrée via la méthode DOMPurify.sanitize
, puis marquez-la comme sûre avec le contexte approprié:
@Pipe({name: 'dompurify'}) export class NgDompurifyPipe implements PipeTransform { constructor(private readonly domSanitizer: DomSanitizer) {} transform( value: {} | string | null, context: SecurityContext = SecurityContext.HTML, ): SafeValue | null { return this.bypassSecurityTrust(context, DOMPurify.sanitize(value)); } private bypassSecurityTrust( context: SecurityContext, purifiedValue: string, ): SafeValue | null { switch (context) { case SecurityContext.HTML: return this.domSanitizer.bypassSecurityTrustHtml(purifiedValue); case SecurityContext.STYLE: return this.domSanitizer.bypassSecurityTrustStyle(purifiedValue); case SecurityContext.SCRIPT: return this.domSanitizer.bypassSecurityTrustScript(purifiedValue); case SecurityContext.URL: return this.domSanitizer.bypassSecurityTrustUrl(purifiedValue); case SecurityContext.RESOURCE_URL: return this.domSanitizer.bypassSecurityTrustResourceUrl(purifiedValue); default: return null; } } }
C'est tout ce qu'il faut pour sécuriser l'application lors de l'insertion de HTML dans la page. + 7Ko gzip de DOMPurify et micropipe. Cependant, puisque nous sommes montés ici, nous allons essayer d'avancer. L'abstraction des classes signifie qu'Angular suggère la possibilité de créer vos propres implémentations. Pourquoi créer un canal et marquer le contenu comme sûr si vous pouvez utiliser DOMPurify tout de suite en tant que DomSanitizer
?

DomPurifyDomSanitizer
Créez une classe qui hérite de DomSanitizer
et délègue l'effacement des valeurs à DOMPurify. Pour l'avenir, nous mettrons immédiatement en œuvre le service Sanitizer
et nous l'utiliserons à la fois dans le tuyau et dans DomSanitizer
. Cela nous aidera à l'avenir, car un seul point d'entrée vers DOMPurify apparaîtra. L'implémentation de SafeValue
et tout ce qui est lié à ce concept est le code privé d'Angular, nous devrons donc l'écrire nous-mêmes. Cependant, comme nous l'avons vu dans le code source, ce n'est pas difficile.
Il convient de noter qu'en plus du HTML, il existe d'autres SecurityContext
et, en particulier, pour le nettoyage des styles DOMPurify en tant que tel ne convient pas. Cependant, ses fonctions sont développées à l'aide de crochets, plus à ce sujet plus tard. De plus, un objet de configuration peut être passé à DOMPurify, et l'injection de dépendances est parfaite pour cela. Ajoutez un jeton à notre code qui nous permet de fournir des paramètres pour DOMPurify:
@NgModule({
@Injectable({ providedIn: 'root', }) export class NgDompurifySanitizer extends Sanitizer { constructor( @Inject(DOMPURIFY_CONFIG) private readonly config: NgDompurifyConfig, ) { super(); } sanitize( _context: SecurityContext, value: {} | string | null, config: NgDompurifyConfig = this.config, ): string { return sanitize(String(value || ''), config); } }
DomSanitizer
travail DomSanitizer
avec les styles est implémenté de telle manière qu'il ne reçoit que la valeur de la règle CSS à l'entrée, mais pas le nom du style lui-même. Ainsi, afin de permettre à notre assainisseur d'effacer les styles, nous devons lui fournir une méthode qui recevra une chaîne de valeur et retournera une chaîne effacée. Pour ce faire, ajoutez un autre jeton et définissez-le par défaut comme une fonction qui n'efface pas la chaîne, car nous ne prendrons pas la responsabilité des styles potentiellement dangereux.
export const SANITIZE_STYLE = new InjectionToken<SanitizeStyle>( 'A function that sanitizes value for a CSS rule', { factory: () => () => '', providedIn: 'root', }, );
sanitize( context: SecurityContext, value: {} | string | null, config: NgDompurifyConfig = this.config, ): string { return context === SecurityContext.STYLE ? this.sanitizeStyle(String(value)) : sanitize(String(value || ''), config); }
De cette façon, nous @HostBinding('style.*')
les styles qui sont directement impliqués dans la liaison via [style.*]
Ou @HostBinding('style.*')
. Comme nous nous en souvenons, le comportement du désinfectant angulaire intégré consiste à supprimer tous les styles en ligne. Honnêtement, je ne sais pas pourquoi ils ont décidé de faire cela, car ils ont écrit des méthodes pour supprimer les règles CSS suspectes, mais si vous devez implémenter, par exemple, un éditeur WYSIWYG, vous ne pouvez pas vous passer de styles en ligne. Heureusement, DOMPurify vous permet d'ajouter des crochets pour étendre les capacités de la bibliothèque. La section des exemples contient même du code qui efface les styles des éléments DOM et de l'ensemble des balises HTMLStyleElement
.
Crochets
À l'aide de DOMPurify.addHook()
vous pouvez enregistrer des méthodes qui reçoivent l'élément actuel à une étape différente du nettoyage. Pour les connecter, nous ajouterons le troisième et dernier jeton. Nous avons déjà une méthode pour nettoyer les styles, il vous suffit de l'appeler pour toutes les règles en ligne. Pour ce faire, enregistrez le hook requis dans le constructeur de notre service. Nous allons passer en revue tous les crochets qui nous sont parvenus en utilisant DI:
constructor( @Inject(DOMPURIFY_CONFIG) private readonly config: NgDompurifyConfig, @Inject(SANITIZE_STYLE) private readonly sanitizeStyle: SanitizeStyle, @Inject(DOMPURIFY_HOOKS) hooks: ReadonlyArray<NgDompurifyHook>, ) { super(); addHook('afterSanitizeAttributes', createAfterSanitizeAttributes(this.sanitizeStyle)); hooks.forEach(({name, hook}) => { addHook(name, hook); }); }
Notre service est prêt et il ne reste plus qu'à écrire votre DomSanitizer
qui ne lui transférera que le contenu pour le nettoyage. Vous devez également implémenter les méthodes bypassSecurityTrust***
, mais nous les avons déjà espionnées dans le code source angulaire. DOMPurify est désormais disponible à la fois de façon ponctuelle, à l'aide d'un canal ou d'un service, et automatiquement dans toute l'application.
Conclusion
Nous avons compris DomSanitizer
en angulaire et ne DomSanitizer
plus le problème de l'insertion de code HTML arbitraire. Vous pouvez désormais intégrer en toute sécurité des messages SVG et HTML à partir du serveur. Pour que le désinfectant fonctionne correctement, vous devez toujours lui fournir une méthode de nettoyage des styles. Les CSS malveillants sont beaucoup moins courants que les autres types d'attaques, le temps des expressions CSS est révolu depuis longtemps et ce que vous faites avec CSS dépend de vous. Vous pouvez écrire un gestionnaire vous-même, vous pouvez cracher et sauter tout ce qui n'a pas de parenthèses, c'est sûr. Ou vous pouvez tricher un peu et regarder de plus près les sources d'Angular. La méthode _sanitizeStyle
est dans le package @angular/core
, tandis que DomSanitizerImpl
est dans le @angular/platform-browser
et, bien que cette méthode soit privée dans son package, l'équipe Angular n'hésite pas à y accéder par son nom privé ɵ_sanitizeStyle
. Nous pouvons faire de même et transférer cette méthode dans notre assainisseur. Ainsi, nous obtenons le même niveau de nettoyage des styles que celui par défaut, mais avec la possibilité d'utiliser des styles en ligne lors de l'insertion de code HTML. Le nom de cette importation privée n'a pas changé depuis son apparition dans la 6ème version, mais vous devez faire attention à de telles choses, rien n'empêche l'équipe Angular de la renommer, la déplacer ou la supprimer à tout moment.
Le code décrit dans l'article est sur Github
Également disponible en tant que package npm tinkoff / ng-dompurify:
https://www.npmjs.com/package/@tinkoff/ng-dompurify
Vous pouvez jouer un peu avec stackblitz avec une démo