Angular Cross Site Scripting
ID |
javascript.angular_cross_site_scripting |
Severity |
high |
Resource |
Injection |
Language |
JavaScript |
Tags |
Angular, AngularJS, CWE:79, NIST.SP.800-53, OWASP:2021:A3, PCI-DSS:6.5.7 |
Description
Angular provides protection against Cross-Site Scripting (XSS) attacks, but that protection could be circumvented by developers for various use-cases that require more direct control on the generated content.
This detector reports where Angular XSS protection is bypassed, or direct modification of the native DOM element is performed.
Rationale
Angular has built-in security protections. It treats all values as suspicious and untrusted by default, which is incredibly helpful because the framework automatically guards us against unintentionally creating vulnerabilities in our applications.
When you add values through interpolation in templates using the {{…}}
syntax, Angular automatically escapes the data. Angular considers tags such as form
, textarea
, button
, embed
, link
, style
or template
as suspicious and may altogether remove the tag or remove specific attributes/child elements. Angular automatically removes any script
tags, for example.
What if you need to bind trusted values that Angular thinks are unsafe? You can mark values as trusted and bypass the security checks, or even directly modify the DOM. Bypassing XSS controls is risky and should be carefully considered.
You can use the DomSanitizer
class in @angular/platform-browser
, to mark values as safe. The DomSanitizer class offers a sanitization method for four types of contexts: HTML, Style, Url, and ResourceUrl. And to mark the value as trusted and safe to use, use one of the bypassSecurity*
methods appropriate for the security context to return a value marked as safe. But be aware of the warning:
calling this method with untrusted user data exposes your application to XSS security risks!
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-trustworthy-image',
template: `
<section [innerHTML]="html"></section>
`
})
export class TrustworthyImageComponent {
// Pretend this comes from an untrusted remote service
public html = `<img src=1 onerror="alert('Doh!')"/>`;
public safeHtml: SafeHtml;
constructor(sanitizer: DomSanitizer) {
// Vulnerable to XSS
this.safeHtml = sanitizer.bypassSecurityTrustHtml(this.html);
}
}
Alternatively, you can modify the DOM directly, the perfect way to cause security vulnerabilities:
import { AfterViewInit, Component, ElementRef, Renderer2, ViewChild } from '@angular/core';
@Component({
selector: 'app-yikes',
template: `
<div #whydothis></div>
`
})
export class YikesComponent implements AfterViewInit {
@ViewChild('whydothis') public el!: ElementRef<HTMLElement>;
// pretend this comes from an untrusted external source
public attack = '<img src=1 onerror="alert(\'YIKES!\')"';
constructor(private renderer: Renderer2) { }
public ngAfterViewInit(): void {
// VULNERABLE - direct DOM manipulation
this.el.nativeElement.innerHTML = this.attack;
// VULNERABLE - same, coded another way
this.renderer.setProperty(this.el.nativeElement, 'innerHTML', this.attack);
}
}
Remediation
Angular is safe against XSS attacks by default, but sometimes one need to bypass the framework’s automatic escaping. General techniques for preventing HTML or JavaScript injection in Angular:
-
Use interpolation with
{{ … }}
to automatically apply escaping by the framework. -
Use safe property binding such as
[href]
,[src]
,[style]
. Use binding to[innerHTML]
to safely insert HTML content. -
Do not use
DomSanitizer.bypassSecurity*()
methods unless you fully trust the input. If such bypass is necessary, a previous sanitization based on a whitelist is the best option.In case you are absolutely certain that input is trusted, mute vulnerabilities reported on
DomSanitizer.bypassSecurity*()
calls so they will be ignored in later scans. -
Avoid modifying the DOM directly, and in particular the
innerHTML
property. When needed to build out the DOM in code usingElementRef
orRenderer2
, useDomSanitizer.sanitize()
to sanitize user input.To fix the vulnerabilities shown before, the code should be modified as follows:
import { Component, AfterViewInit, ViewChild, ElementRef, Renderer2 } from '@angular/core';
import { DomSanitizer, SecurityContext } from '@angular/platform-browser';
@Component({
selector: 'app-way-better',
template: `
<div #waybetter></div>
`
})
export class WayBetterComponent implements AfterViewInit {
@ViewChild('waybetter') public el!: ElementRef<HTMLElement>;
// pretend this is from an external source
public attack = '<img src=1 onerror="alert(\'YIKES!\')"';
constructor(private renderer: Renderer2, private sanitizer: DomSanitizer) { }
public ngAfterViewInit(): void {
// FIXED, the input is sanitized before modyfying the DOM
const cleaned = this.sanitizer.sanitize(SecurityContext.HTML, this.attack);
this.renderer.setProperty(this.el.nativeElement, 'innerHTML', cleaned);
}
}
References
-
CWE-79 : Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
-
OWASP Top 10 2021 - A03 : Injection.
-
Protect Your Angular App From Cross-Site Scripting, from Okta SPA Security Series. The code examples (1), (2) and (3) were adapted from this source.