The Complete Guide to Angular Security

Share on facebook
Share on google
Share on twitter
Share on linkedin

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.

Image result for owasp top 10 2020 vs 2017

Of these threats, the ones that relate to Angular development are:

  1. Cross-Site Request Forgery (CSRF)
  2. Sensitive Data Exposure
  3. Cross-Site Scripting
  4. 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 writesYesLinks, redirects, form submissions
Cross-Origin readsNoNone
Cross-Origin embeddingYesScripts, CSS, Images
SOP: What is allowed?

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):

Image result for cors error

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.

Image result for cors preflight

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-urlencodedmultipart/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).

CSRF steps illustration

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:

  1. Send a custom header on each request (client)
  2. 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.

sensitive data exposure

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.

Image result for sensitive data exposure

A man in the middle attack can occur even with HTTPS as we will see:

  1. Requesting website, where first request per default is an http request
  2. Requests HTTP page on server
  3. Server redirects to HTTPS
  4. Man in the middle requests HTTPS page
  5. The server responds with an HTTPS response
  6. Interceptor rewrites HTTPS to HTTP and can inject malicious code
  7. 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.

Image result for hacked dns

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.

Image result for Cross-Site Scripting

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.

Clickjacking Attacks: What They Are and How to Prevent Them | Netsparker

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.

Image result for Using Components with Known Vulnerabilities

Examples

  1. Contributes to npm with a useful library.
  2. Projects start to use the library.
  3. Adds malicious code to his library.
  4. 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.

Related Posts and Comments

How to Set up a CI pipeline with Azure Pipelines and Nx

It goes without saying that having a CI pipeline for your Angular apps is a must. Setting one up for regular Angular apps is fairly straightforward but when you have an Nx monorepo there are certain other challenges that you have to overcome to successfully orchestrate a “build once, deploy many” pipeline. This post will

Read More »

How to Set Up Git Hooks in an Nx Repo

Git hooks can be used to automate tasks in your development workflow. The earlier a bug is discovered, the cheaper it is to fix (and the less impact it has). Therefore it can be helpful to run tasks such as linting, formatting, and tests when you are e.g. committing and pushing your code, so any

Read More »

The Stages of an Angular Architecture with Nx

Long gone are the times when the frontend was just a dumb static website. Frontend apps have gotten increasingly complex since the rise of single-page application frameworks like Angular. It comes with the price of increased complexity and the ever-changing frontend landscape requires you to have an architecture that allows you to scale and adapt

Read More »

The Best Way to Use Signals in Angular Apps

Since Angular 16, Angular now has experimental support for signals and there is a lot of confusion in the community about whether this is going to replace RxJS or how it should be used in an app in combination with RxJS. This blog post sheds some light on what I think is the best way

Read More »

High ROI Testing with Cypress Component Testing

Testing is one of the most struggled topics in Angular development and many developers are either giving up testing altogether or applying inefficient testing practices consuming all their precious time while giving few results in return. This blog post will change all this as we will cover how I overcame these struggles the hard way

Read More »