Want to learn more about OAuth 2.0 and OpenID Connect?
Save yourself days of digging through dozens of specs with this online course
More informationPreventing XSS in Angular
A Cross-Site Scripting (XSS) vulnerability can and will lead to the full compromise of a frontend application. An XSS vulnerability allows the attacker to control the application in the user's browser, extract sensitive information, and make requests on behalf of the application. Modern frameworks come with built-in defenses against XSS, but how far do they really go? In this article, we look at Angular's built-in XSS defenses, along with insecure coding patterns that inadvertently bypass these protections.
1 February 2021 SPA Security XSS, Angular, Single Page Applications
A short primer on XSS
An XSS vulnerability is an injection vulnerability, where the attacker inserts a piece of malicious data into a web application. The maliciously injected data will be picked up by the browser, which interprets the data as code. This causes the injected payload from the attacker to be executed as legitimate application code, which gives the attacker full control over the application running in the user’s browser.
The code example below illustrates a textbook example of an XSS vulnerability and attack.
A DOM-XSS vulnerability in a jQuery application
<div id="messageContainer"></div>
<script>
...
$("#messageContainer").append(myMessage);
...
</script>
A malicious message containing a JavaScript payload
Hello John!<script>alert("Let's play hide and seek!")</script>
If this data is inserted into the page, the browser executes the script in the message, triggering the alert dialog. The example here calls the alert()
function, which is probably the least dangerous effect of an XSS attack. Make no mistake. In reality, attackers can steal sensitive data from the pages, capture user input to steal passwords or credit card information, and even send requests to servers as if the legitimate application itself sends them. Once an attacker succeeds in exploiting an XSS vulnerability, you should consider the application’s entire execution environment in the browser to be compromised.
The best defense against XSS attacks is to ensure that the browser will never see data as code. A common approach to achieve that is by applying context-sensitive output encoding. Context-sensitive output encoding will ensure that the data is encoded for the context where it ends up in the page. During the encoding, potentially dangerous characters are translated into harmless counterparts. Because of that translation, the browser will see these characters as data instead of code, which avoids the confusion and prevents the attack.
Below is an example of the effect of applying context-sensitive output encoding on data containing a malicious payload.
A malicious message containing a JavaScript payload, but properly encoded to avoid XSS
Hello John!<script>alert("Let's play hide and seek!")</script>
As you can see, the browser is now instructed to render the <
and >
HTML codes, which display the <
and >
characters. Since this is clearly data, the browser will not confuse the data for code.
Angular applies auto-escaping
When an application puts data into the page using Angular’s interpolation mechanism (i.e., using {{ }}
), Angular is aware of the potential danger of XSS. As a result, Angular will automatically ensure that data that ends up in the page will not cause XSS attacks. The code snippet below shows such a data binding.
Angular auto-escapes all data bindings that rely on interpolation
<p>{{review}}</p>
This auto-escaping defense mechanism, which Angular calls Strict Contextual Escaping, is crucial to ensure baseline protection against XSS attacks. When server-side frameworks and client-side frameworks fail to offer this minimal level of protection, applications using those frameworks often suffer from a vast amount of XSS vulnerabilities.
Angular applies this context-sensitive behavior on all data bindings that rely on interpolation. For example, binding data into an HTML attribute will ensure that data cannot escape from the attribute and trigger the execution of additional script code.
When escaping takes it a bit too far
Real-world applications often run into requirements where they need to render dynamic HTML code. That HTML code typically originates from untrusted sources, such as user-provided data. The image below illustrates one common scenario of this use case.
Rich text editors, such as CKEditor, typically generate HTML as output. The document you can see above displays images with <img>
elements, headers with an <h1>
tag, and paragraphs of text with the <p>
tag. To render the output of the editor correctly, the browser needs to be able to parse and render the HTML code.
In my training courses, I use a training application that collects restaurant reviews. The application wants users to be able to use benign HTML constructs in their reviews. While reviews are less elaborate than CKEditor documents, the technical requirements are the same.
For these use cases, Angular exposes the [innerHTML]
property. Note the square brackets here, which indicate that this is an Angular property, not the native DOM API’s innerHTML
property. By binding values to [innerHTML]
, we instruct Angular to automatically sanitize the data before putting it into the page. You can see an example payload and the rendered data below.
User-provided data is a common attack vector to exploit XSS vulnerabilities
This restaurant is absolutely horrible.
The service is <b>slow</b> and the food is <i>disgusting</i>.
<img src="nonexistent.png" onerror="alert('This restaurant got voted worst in town!');" />
If we run this review through the Angular sanitizer, it would come out as the review you can see below.
The user-provided review after it has been sanitized by an HTML sanitizer
This restaurant is absolutely horrible.
The service is <b>slow</b> and the food is <i>disgusting</i>.
<img src="nonexistent.png" />
The mechanism at work here is HTML sanitization using Angular’s built-in HTML sanitizer. The HTML sanitizer knows which elements and attributes are safe (e.g., <b>
and <i>
) and which are potentially unsafe (e.g., <script>
and the href
attribute of an <a>
element). A sanitizer will leave all the safe pieces of HTML in the data but scrub out all the dangerous parts.
Note that sanitization sounds a lot like filtering (a.k.a, the stuff you do with regexes), but it is an entirely different approach. A sanitizer will parse the data into a syntax tree and make security decisions based on that tree. A good sanitizer uses a list of safe values and considers everything that is not on the list unsafe.
So we can’t mess things up in Angular?
Angular does a stellar job in helping developers write secure code. Anyone building Angular applications the “normal way” can rest assured that they will not cause XSS vulnerabilities. Finding a way to sidestep Angular’s defenses takes quite a bit of effort. Let’s look at a few patterns you might want to avoid in your applications.
Bypassing Security
Angular offers a way to output raw HTML without any XSS protections applied. This functionality is available through the function bypassSecurityTrustHtml()
. As the name of the function implies, this mechanism completely bypasses Angular’s security mechanisms. Using it on untrusted data will result in XSS vulnerabilities.
The code example below shows how to use the bypassSecurityTrustHtml()
function.
Marking a static code snippet as safe using the bypassSecurityTrustHtml function
constructor(private sanitizer : DomSanitizer) {}
getHtmlSnippet() {
let safeHtml = `<img src="..." onerror="alert('Failed to load image')">`;
return this.sanitizer.bypassSecurityTrustHtml(safeHtml);
}
Assigning a safe snippet to [innerHTML] does not trigger Angular's sanitizer
<div [innerHTML]="getHtmlSnippet()"></div>
This behavior is extremely dangerous when misused, as the name of the function suggests. The only acceptable use case would be to output a static piece of code, something you have written yourself. Never use this function with any untrusted value.
Additionally, if you need to use it for outputting static code, consider encapsulating this function in a helper function with an apt name (e.g., iPromiseThereIsNoUserDataAndOnlyStaticHtml()
). Such a name clearly conveys this function’s purpose to avoid any problems with misuse or refactoring later. Additionally, this coding pattern allows you to ban the use of bypassSecurityTrustHtml()
in your codebase, except for that single helper function.
Using native DOM elements
Angular works on the level of components and not really on the level of individual HTML elements. Sure, templates contain HTML elements, but Angular applications rarely reference these elements explicitly from code. Rarely is the keyword here. Most applications don’t need to directly access HTML elements, but some specific use cases require such low-level access.
That’s precisely why Angular supports the use of an ElementRef. The code example below shows how to reference an element in the component’s code using ElementRef. This element is defined a template as <div #myDiv></div>
.
Using an ElementRef to refer to a specific HTML element
@ViewChild("myDiv") div : ElementRef;
With this reference, we can now access the native DOM element. Such access is useful for detailed event handling but also allows insecure coding patterns, such as the code example below.
Dangerous behavior using native DOM elements
this.div.nativeElement.innerHTML = this.inputValue;
In this code example, we put data directly into the DOM. Since we’re dealing with native DOM APIs, Angular is no longer able to protect us from potential XSS attacks. This code is extremely insecure and sidesteps Angular’s built-in XSS defenses.
There is no good reason why a code pattern like this should ever be used in an Angular application. I recommend scanning your codebase to ensure this pattern is not present and set up a linting rule to ensure this pattern will never make it into the codebase.
Using the Renderer2 API
The use of innerHTML
on native DOM elements is not the only way you can sidestep Angular’s secure-by-default defenses. With an ElementRef, you can also use the Renderer2 API to manipulate the DOM. This mechanism is perhaps even more dangerous since the Renderer2 API is a legitimate Angular API. Unfortunately, this API does not apply automatic XSS protections and should be avoided as much as possible.
The code example below shows how to use the Renderer2 API with an ElementRef to set the innerHTML
property of an element, which bypasses Angular’s XSS defenses.
Dangerous behavior using the Renderer2 API
@ViewChild("myDiv") div : ElementRef;
constructor(private renderer2 : Renderer2) {}
loadDivWithRenderer2() {
this.renderer2.setProperty(this.div, "innerHTML", this.inputValue);
}
Just like with using native DOM APIs, I recommend scanning your codebase to ensure this pattern is not present and set up a linting rule to ensure this pattern will never make it into the codebase.
This 2-day security workshop goes a lot further than Angular's built-in XSS defenses
Join this workshop to learn more about current security best practices for Angular applications
More informationWait a minute, what about URLs?
Another common source of XSS vulnerabilities are URLs. Take a look at the URL shown below.
All modern browsers support JavaScript URLs in element attributes
javascript:alert('Don't laugh, this is not a joke!')
This URL uses javascript:
as the scheme, instead of http:
or https:
. When a browser sees such a URL in an HTML document, it typically sees it as JavaScript code that needs to be executed. You can try out what happens if a javascript:
URL ends up in the href
of an a
element by clicking here. When a user can control such a URL, it can be used to trigger the execution of malicious JavaScript code.
Today, it is safe to say that javascript:
URLs are a painful mistake from the past. Unfortunately, applications have come to rely on this behavior for legitimate features, making it difficult to turn this off at the browser level. However, more and more modern application frameworks are discouraging or preventing the use of javascript:
URLs. Angular does precisely that through a specific URL sanitizer.
The Angular sanitizer ensures that dynamically-created URLs are safe to use in the application. A look at the code reveals that the sanitizer only allows known safe URLs and prefixes other URLs with the unsafe:
scheme. This mechanism effectively prevents XSS through URLs.
Even better, Angular applies this mechanism out of the box!
Not so fast, resource URLs don’t work out of the box!
If you’ve ever tried to load an iframe by assigning a variable to the [src]
property in Angular, you have likely encountered an error like the one shown below.
This is Angular protecting the application from potentially harmful content. Good, but why the error message?
Resource URLs are essentially URLs that immediately trigger the loading of a resource. Examples are iframes, scripts, stylesheets, and so on. Including javascript:
URLs in such a location can be dangerous, but there’s more. Where do you want to load such active content from? Do you want to load an iframe from https://evil.example.com/hackme.html
?
Instead of guessing what is safe and what is not, Angular errs on the safe side by denying the dynamic loading of resources. That’s why you get the error message. So, in essence, Angular protects you against these dangerous patterns out of the box.
To enable this behavior, your application will have to vouch for the URL’s validity you are trying to load. In essence, this means you will have to ensure the URL is constructed safely, or you will have to validate the URL before assigning it to an HTML element. To prevent Angular from triggering the error, you have to mark the URL as safe before assigning it. You can do that by using the bypassSecurityTrustResourceUrl()
function, as shown below.
Turning a dynamic URL into a safe URL using the bypassSecurityTrustResourceUrl function
private getYoutubeVideo(input : string) : SafeResourceUrl {
// Always define scheme, host and path separator
const host = "https://www.youtube.com/embed/";
// Additional check to ensure the URL is safe to use
let url = this.sanitizer.sanitize(SecurityContext.URL, host + input);
// Mark the URL safe to use in a resource URL context
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
Note that in this code example, we construct a safe Youtube URL by only allowing the user to provide a video ID. That way, we are sure that the URL points to https://www.youtube.com/
. Make sure your application follows a similar pattern to ensure the safety of the URL. Do not mark an unknown URL as safe since this may introduce vulnerabilities in your applications.
And what about server-side page rendering
The beauty of Angular is that it’s a framework addressing common use cases. As a result, the Angular code running in the browser is the same code running on the server. While this seems logical, this is not always true for other frameworks, such as React (Check out this 3-part article series on XSS in React).
Concretely, for Angular, this means that if you follow secure coding guidelines, as discussed in this article, your server-side rendered pages are also secure. Angular Universal also protects against other vulnerabilities out-of-the-box, which is great. Unfortunately, the same is not always true for projects based on Angular. You can read more about that in this article about XSS through JSON.stringify()
, a vulnerability that was present in Scully.
Summary and secure coding guidelines
That brings us to the summary of this article on XSS in Angular applications. To wrap it up, let’s recap the most important takeaways.
- If you stick to the Angular way of doing this, Angular will help you guarantee the security of your application.
- To ensure you let Angular do its job, avoid direct DOM manipulation through ElementRef, the Renderer2 API, or native DOM APIs.
- Make sure your application does not use a
bypassSecurityTrust*()
function with dynamic data.
That’s it. These three simple guidelines will help you avoid XSS in Angular. And if you liked this article, don’t forget to share it with your Angular friends and colleagues.
Want to learn about defense-in-depth mechanisms for Angular applications?
Join this workshop to learn more about current security best practices for Angular applications
More informationAbout Dr. Philippe De Ryck
Hi, I'm Philippe, and I help developers protect companies through better web security. Learn more about my security training program, advisory services, or check out my recorded conference talks.
Want to learn more about OAuth 2.0 and OpenID Connect?
Save yourself days of digging through dozens of specs with this online course
More informationDr. Philippe De Ryck
Hi, I'm Philippe, and I help developers protect companies through better web security. As the founder of Pragmatic Web Security, I travel the world to teach practitioners the ins and outs of building secure software.
Talks and workshops
You will often find me speaking and teaching at public and private events around the world. My talks always encourage developers to step up and get security right.
Articles
Security is often about small nuances. In my articles, I dive deeper into various security topics, providing concrete guidelines and advice. My articles also answer questions I often get while speaking or teaching.
Security resources
Getting security right is all about knowledge. I strongly believe in sharing that knowledge to move forward as a community. Among my resources, you can find developer cheat sheets, recorded talks, and extensive slide decks.
Mailing list
Subscribe to the Pragmatic Web Security mailing list to stay up to date on the latest activities and resources.