Many frontend developers might see security as a non-functional concern, that is handled by the security department and that they instead should just focus on feature development. The reality is that learning about security actually forces you to gain knowledge about how the browser works thus it can make you a better Angular developer in general.
High-performing teams, in this day and age, are usually autonomous DevOps teams and use continuous delivery for faster feature delivery. This way of working is only possible if we, frontend developers, also take responsibility for the security as we build features.
This post aims to give you the know-how to build security into your feature development and become a better developer by gaining a higher understanding of the browser and security concepts that are relevant for Angular developers.
I will go through the OWASP top 10 security threats that relate to Angular development and explain how to mitigate each one in this post.
What is OWASP top 10?
The OWASP top ten is the top ten list of web security risks from the Open Web Application Security Project foundation. It consists of a group of security experts who release a top 10 list of relevant security risks every 3-4 years which many companies use to focus their security effort.
OWASP 2013 to 2017
The OWASP top ten has evolved through the years and has gotten rid of a couple of security risks, that are no longer relevant enough to make the top ten in the 2017 edition.
Of these threats, the ones that relate to Angular development are:
- Cross-Site Request Forgery (CSRF)
- Sensitive Data Exposure
- Cross-Site Scripting
- Using Components with Known Vulnerabilities
The other threats are mostly handled on the backend and are not covered in this post.
Basic security concepts
Let’s look at some basic security concepts to improve our understanding of how the browser works.
Same-Origin Policy
The same-origin policy (SOP) is a security concept that limits how one origin can interact with other origins.
An origin consists of a protocol, domain and port:
protocol://domain:port
eg. https://google.com:443
In a same-origin policy, reads are typically prohibited across origins and writes are typically allowed across origins.
What? | Mostly Allowed? | Allowed Operations | |
Cross-Origin writes | Yes | Links, redirects, form submissions | |
Cross-Origin reads | No | None | |
Cross-Origin embedding | Yes | Scripts, CSS, Images |
For writes, links, redirects, and form submissions are allowed. The general rule of thumb with SOP and cross-origin writes is; everything that you can do with an HTML form is allowed
The general rule of thumb with SOP and cross-origin writes is; everything that you can do with an HTML form is allowed.
Reading from another origin is not allowed per default (more on CORS headers soon). Doing so will result in this error (you try to fetch from another origin):
Embedding is generally allowed when embedding scripts, CSS, and images thus are normally a common attack surface as well (as we will see later).
Implicit and explicit authentication
Implicit authentication means the authentication is based on something the browser automatically (implicitly) sends on each request. This could be cookies, HTTP basic auth, and TLS client certificates.
The problem with this kind of authentication is, it is possible for an attacker, to do authenticated writes on behalf of a user if the servers are not explicitly protecting against this (eg. cross-site request forgery attacks, as we will soon see more about).
Explicit authentication means manually (explicitly) sending the authentication token by the developer such as via an HTTP header. The most common way to do this is using OAuth bearer tokens in the headers.
Cross-origin resource sharing
Cross-origin resource sharing is a mechanism, controlled by HTTP response headers on a server, for allowing origins to perform “non-HTML form compliant” requests to the server. On such a request, the browser will first do a preflight request, for loading the CORS headers and then do the actual request.
The browser automatically sends the preflight (OPTIONS), if this is a cross-origin non-form request, for reading the Access-Control-Allow-Origin
header. The client will proceed with the actual request if the header is present and valid (needs to either contain the client’s origin or *).
Here’s an example preflight request and response:
A “non-HTML form compliant” request, is a request that contains custom headers or content-type
other than the standard HTML form content types:
application/x-www-form-urlencoded
, multipart/form-data
, or text/plain
The best practice with cross-origin resource handling is to whitelist the origins, which are permitted to call the server instead of using wildcard *.
Don’t: Access-Control-Allow-Origin: *
Do: Access-Control-Allow-Origin: https://somedomain.com
Cross-Site Request Forgery
CSRF attacks involve the attacker taking advantage of implicit authentication such as a session cookie to make you do an authenticated request to a site.
CSRF attacks are handled automatically by many frameworks and mechanisms such as CMS and OAuth2 so is no longer a big enough threat to make it in 2017 OWASP top ten but is still worth knowing how to mitigate against in Angular apps.
A typical CSRF attack involves an attacker hosting a site, which is doing requests to a target website, once we user visits the attacker site. This will work if the target site uses implicit authentication and the user is already authenticated on the target site (and there is no CSRF protection).
Due to SOP this attack will only work when:
- Write operation (can be GET or POST)
- Standard HTML form content types:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- Implicit authentication
- Only standard headers
If the server contains Access-Control-Allow-Origin: *
we would only require implicit authentication to do a CSRF attack (did I tell you to not use wildcards with your CORS?) as non-standard headers and non-form content-type are what is causing a CORS preflight to be necessary.
Examples
User visits attacker site, which performs a form request to bank.com in which the user is authenticated using a session cookie. The site can now trigger eg. a payment on behalf of the user’s account.
Solution
For the most part, CSRF is not a security threat anymore as most Angular apps use explicit authentication such as OAuth2.
If your app is using implicit authentication eg. using a cookie, then the simplest way to protect yourself against CSRF attacks is to:
- Send a custom header on each request (client)
- Verify the custom header is present on each request (server)
Because custom headers can’t be sent across domains without a CORS preflight this will protect you against CSRF if you are not using Access-Control-Allow-Origin: *
.
1. Send a custom header on each request (client)
Angular’s HttpClient
automatically sends a X-XSRF-TOKEN header on each request when performing a mutating request, such as POST.
2. Verify a custom header is visible on each request (server)
For Node Express servers, this is easily handled by the middleware csurf which will check for the CSRF header being present on each request.
Sensitive Data Exposure
Sensitive data exposure is about having sensitive data read by an attacker, either while in transit, such as for a man-in-the-middle attack, or by tampering with the requested page eg. if the DNS is hacked.
Man in the middle
A man in the middle attack involves a mediating network device, that is able to read unencrypted data and tamper with the returned payload when a site is requested.
A man in the middle attack can occur even with HTTPS as we will see:
- Requesting website, where first request per default is an http request
- Requests HTTP page on server
- Server redirects to HTTPS
- Man in the middle requests HTTPS page
- The server responds with an HTTPS response
- Interceptor rewrites HTTPS to HTTP and can inject malicious code
- The clients future requests are now HTTP can now be sniffed and tampered with by the man in the middle
The basic problem here is the first request is unencrypted and can thus be tampered with by the man in the middle.
There are two solutions to this; VPN and HSTS header.
VPN is not a security mechanism you can set up on your own site, so let’s focus on the HSTS header.
Solution
The solution is to ensure that the first request is HTTPS by using HTTP Strict Transport Security headers.
To do this, you make your frontend server return a Strict-Transport-Security
header.
By setting the expire-time more than one year, and setting includeSubDomains and preload, the site will be part of the browsers preload list which is a hardcoded list in browsers ensuring the site will only be requested over HTTPS. You can check if your domain is in the preload list here. Otherwise, the clients will perform an HTTP request every time the HSTS header expires.
Hacked CDN
Hacked DNS involves the attacker getting control over a DNS server and is able to point domains to other servers eg. their own malicious website.
This malicious website might be, UI-wise, a clone of the original site but containing logic for eg. sniffing logins and credit cards.
Solution
The solution to protecting your users against compromised DNS is to use the integrity attribute in the script tag, loading the DNS resource, which is a checksum mechanism to ensure the integrity of what the DNS serves:
Now you will get an error if the integrity is compromised in the CDN. This is supported by many DNS providers.
Cross-Site Scripting
Cross-site scripting is one of the most dangerous threats as it involves an attacker taking control over your app’s javascript by executing javascript from an input source in the app such as the data storage and URL.
Example – Samy the myspace worm
One of the most famous XSS attacks is MySpace’s Samy worm. A 19-year-old man Samy found out, you could inject HTML to your profile page, including script
tags, for taking control over the javascript of users visiting your profile page.
He used this to inject visitors with a worm that wrote “…and most of all samy is my hero” and add Samy as their friend. To make it spread exponentially, he also made sure the visitors of his visitors would get the same worm.
He ended up getting over a million friend requests and MySpace had to take down their site for a couple of hours to find out what was going on. Samy ended up getting a 3 years “no internet” probation.
This worm was a tipping point for web security as it imposed more focus on cross-site scripting threats that appeared to be present on many other sites.
Sources and sinks
A source is the user input, that can cause XSS vulnerabilities when they are not validated and/or sanitized. A source should be validated and a sink should be sanitized to stay safe.
XSS attacks involve some kind of user input (source), that is being executed by a sink in the code. A sink is where user input can be executed, leading to XSS volatilities.
These sinks include passing a function as a string to any of these functions:
eval()
setTimeout()
setInterval()
Function()()
Or any default Javascript method that updates the Dom as you can eg. inject script tags:
document.write()
document.writeln()
document.domain
someDOMElement.innerHTML
someDOMElement.outerHTML
someDOMElement.insertAdjacentHTML
someDOMElement.onevent
For that reason, it is recommended to avoid any of these sinks in Angular apps and instead follow the “Angular way” and let Angular take care of DOM manipulation and avoid passing strings to any of the sinks (eg. setTimeout
).
Also, in case of an XSS attack, we want to limit the damage eg. by ensuring sensitive data can’t be read from Javascript.
Content Security Policy
Content security policies or CSP is an HTTP response header on the host server for protecting against cross-site scripting attacks. This header, got a couple of directives for whitelisting resource sources eg. determining which domains it is allowed to load scripts and iframe sources from.
Eg. for allowing only scripts to be loaded from current origin and https://example.com as well as iFrames only to load form origin site and https://youtube the CSP would be:
Content-Security-Policy: script-src 'self' https://example.com/ iframe-src 'self' 'https://youtube.com'
This header should be returned from your hosting server as you serve the Angular app.
You can read more about CSP here.
Using CSP to protect against click jacking attacks
A clickjacking attack is where your site is being embedded invisibly on the attacker’s site, so when you eg. are trying to click on a button on the attacker’s site, you are actually clicking “Pay” on your own site where you might be authorized to do this action.
CSP headers can also be used to protect against clickjacking attacks by restricting where your site can be embedded:
Content-Security-Policy: frame-ancestors 'none';
But you can also do this without CSP:
X-FRAME-OPTIONS: SAMEORIGIN
How Angular sanitizes
Angular does the heavy lifting when it comes to XSS security (which is why you should stay away from mutating the DOM manually eg. using the DOM API or jQuery).
Inner HTML will allow some “safe” tags to be rendered and unsafe tags will be removed eg. script
tags.
Interpolation will not do anything with the tags and the string will be shown as it is written in code.
The best way to stay free from XSS attacks is to only update the DOM through Angular’s interpolation (did I mention that?).
Angular’s built-in protection will also prohibit doing other things that could cause an XXS vulnerability, such as passing a dynamic URL to eg. an iframe, image, or script tag.
Bypassing Angular’s sanitization
Angular also contain methods to bypass it’s security for certain scenarios:
There is one situation, where we need to bypass Angular’s security and that is when we want to pass a dynamic URL to an iFrame.
In this case, the iFrame’s URL will need to bypass Angular’s security so it is trusted by Angular or we get:
Also, when we are bypassing Angular’s security, we need to make sure to sanitize the input ourselves as well as whitelist the possible URL’s to be injected through CSP frame-src.
Trusted Types
Trusted Types is a CSP directive for protecting against XXS by specifying trusted types that are allowed to be passed to sinks (as passing inputs to sinks are the root of XXS attacks).
A trusted type is a value that typically has undergone some kind of sanitization and is marked as safe to use in sinks by the browser.
Note: currently Angular apps are not compatible with Trusted Types if you use lazy loading, as webpack is trying to set a sink (src) with an untrusted type and you will get this error:
That means for most cases, you can’t use Trusted Types in your Angular apps yet. Once the webpack/Angular issue is fixed, we can use the theory in this section.
How to use Trusted Types
To learn about Trusted Types from a more pragmatic standpoint, let’s look at how to use Trusted Types in your app.
Setup trusted-types directive in CSP header
We can locally set the CSP header to enable Trusted Types:
Content-Security-Policy: trusted-types; require-trusted-types-for 'script'; report-uri
This will cause a security error, if you pass any untrusted value to a sink and report it to the report URI.
We can also specify a whitelist of policies to allow for creating the Trusted Types:
Content-Security-Policy: trusted-types custompolicy1 custompolicy2; require-trusted-types-for 'script'; report-uri
This means; require Trusted Types for DOM XXS injection sink functions, and these Trusted Types need to be created with either policy customtype1
or customtype2
.
This leads us to how to create these Trusted Type policies.
Define a Trusted Type policy that matches trusted-type name in header
If no Trusted Type policy is specified in the header, you can just use the library DOMPurify to sanitize and return trusted type:
import DOMPurify from 'dompurify'; el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});
Otherwise, if you don’t want/can’t depend on a third-party library, you can create a custom policy to handle sanitization and return a Trusted Type:
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing const escapeHTMLPolicy = trustedTypes.createPolicy('custompolicy1', { createHTML: string => string.replace(/\</g, '<') }); }
And use it like:
Create default Trusted-type (handles cases without a specific trusted type)
You can use the default policy to automatically sanitize strings passed to a sink which is sometimes necessary to circumvent Trusted Type violations coming from a third-party library (as you can’t use your custom policy in them):
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
trustedTypes.createPolicy('default', {
createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
});
}
Note, however, that having global code like this is not recommended as it breaks encapsulation. Instead, it is best practice to first try to get by with DOMPurify (with no specified Trusted Type policies) or custom policy (specifying Trusted Type policies in the header).
Angular’s Trusted Types
Angular supports it’s own set of custom type policies:
- Angular: Treats inline template values and sanitized values as safe
- angular#unsafe-bypass: Allows bypass sanitize methods
- angular#unsafe-jit: Allows jit compiler operations or running in jit
These can be used to security review your app and make it explicit eg. if you want to allow unsafe bypass and using the jit compiler.
For specifying that neither unsafe-bypass nor jit compiler is allowed in the app, you can set the following CSP header:
Content-Security-Policy: trusted-types angular; require-trusted-types-for 'script';
Avoiding sniffing of sensitive data
In case of an XSS attack, we want to mitigate the damage, so we eg. don’t let the attacker collect logins and credit cards.
We do this by having sensitive data such as logins and credit card forms separated from the site by displaying it in an iFrame hosted on another origin as Javascript can’t read the DOM of iFrames across origins (can only communicate through certain methods such as custom events).
This method is common but is still vulnerable to XXS attacks if you eg. place a key sniffer over the iFrame to collect the user input.
A better and more safe method is to do a full redirect to the login or payment site (hosted on another origin), as this will make it impossible for XXS attacks to access these separate sites.
Using Components with Known Vulnerabilities
This involves using third-party libraries, that either has security flaws or that are intentionally trying to do harmful things to us (eg. collect sensitive data, mine crypto, or spy on the user). In this day and age, with our apps consisting primarily of third-party code, this makes this one of the most dangerous threats as well.
Examples
- Contributes to npm with a useful library.
- Projects start to use the library.
- Adds malicious code to his library.
- Projects update the library and are now running the malicious code.
Solution
To mitigate risks from third-party library I advise you to:
Scan npm packages using npm audit. This can let you know if your packages contain known vulnerabilities.
Be extra careful with third-party libs, that are not backed by a trusted organization + not widely adopted.
Mitigate XSS attacks in case of a breach as explained in the previous section (protect sensitive data, use Trusted Types, and CSP).
Conclusion
In this post, we went through the threats in the OWASP top 10 that are relevant to Angular developers.
We covered security concepts such as SOP and CORS. We also looked into threats such as CSRF, sensitive data exposure, XXS, and components with known vulnerabilities.
Learning this stuff will help you develop more secure Angular applications and might save your company millions by avoiding a breach.
In Angular Architect Accelerator, I cover these security concepts and more with step-by-step code demonstrations, so you know exactly how to follow the security best practices with Angular apps. You can sign up for a free warmup workshop already now.
Resources
Do you want to become an Angular architect? Check out Angular Architect Accelerator.