Frontend security primer

Pragmatic principles to help build secure frontends. Learn React application XSS security best practices.

Rem · 12 Aug 2022

Updated · 24 April 2023

Share:

Introduction

Security is as important as ever. Despite this, frontend security often takes the back seat compared to shipping fast, and being performant.

This is in part because modern browsers and frontend frameworks providing a decent amount of security by default.

Technologies we often take for granted like https and auto-escaping template engines eliminate large classes of attacks for us.

While this is great, it can lead to a sense of complacency when it comes to building secure frontends if we aren’t vigilant.

Security in general, is deeply nuanced and broad. Which can feel intimidating to approach sometimes.

There’s tons of articles and cheat sheets available online, that can help build a working knowledge of common exploits.

In this article we’ll take a slightly different approach. We’ll look at how we can start to develop more secure frontend code by default, without necessarily having an encyclopedic knowledge of exploits.

We’ll then take a look at developing a kind of frontend security spidey sense for the most common frontend security issue, cross site scripting (XSS).

Cross site scripting is consistently in the top 10 OWASP common attacks on web applications. As a frontender it’s worth getting well acquainted with this dark art, given the likely hood of encountering it in the wild some day.

We’ll use React for the specific examples, but the principles outlined can also apply to any frontend framework.

Thinking like a hacker

Computer scientists focus on worst case scenarios when designing algorithms and data structures. This mindset helps keep the code efficient and scale to large inputs.

As we gain more experience, we often start to develop a kind of vigilance developed from past mistakes and experiencing approaches that didn’t go well.

This creates a kind of background process in our minds when writing or reviewing code.

How’s this approach going to evolve over time? What’s going to go wrong here when this hits production?

The good news is we don’t always have to learn the hard way. We don’t need to have our frontends continually hacked before we develop a security focussed background process.

We just need to ask the right questions to put us on the right track. The same sort of questions a seasoned hacker may ponder when attempting to break into a system.

In other words to put ourselves in the shoes of a malicious hacker. Compared to computer scientists who are systematic in their analysis, a hacker takes a no holds barred approach to finding any potential vulnerability and exploiting it to gain a foothold.

Let’s see what this means in the form of questions we can start to ask when writing or reviewing code:

Asking these types of questions doesn’t guarantee we’ll have secure, bug free code. There’s no silver bullet. But it does take us a long way towards building more secure and resilient code than we would otherwise have.

While it’s important to have working knowledge of the different types of security exploits out there (check the references at the end of this article for more resources to build this up). Defending against those begins with having developed a security frame of mind from the very beginning.

Alright, with that out the way. Let’s now turn to a practical principles for dealing with the most common type of frontend security breach we are likely to encounter on the frontend.

It’s estimated that 60% of frontend security attacks are some sort of XSS attack. It’s up there as the top security risk in modern frontend applications. With even security focussed apps like Signal, falling prey to XSS vulnerabilities in their desktop application.

A quick XSS recap

We’ll start with the same origin policy. A foundational security principle that browsers implement.

The same origin policy prevents some random running their Javascript on our site without our permission. Which turns out to be pretty important for the web.

So attackers need to get their Javascript running on our pages some other way. Without proper security there’s a lot of ways they can do this.

If an attacker can achieve this, the game is usually over. They gain the ability to do whatever the user can do through their browser, controlled by the power of whatever Javascript can do in a browser.

So this includes things like getting sensitive credentials, sending any authorized requests, and all sort of other malicious things you can think of that can get them full system access.

A classic example of XSS, is the story of the guy who wrote the Myspace “Sammy worm”.

This was a script that got added to Myspace profile pages, that added him to their friends list, and top of their heros section.

”Once I was able to do that, I realized I was able to actually do anything on the page”

Any time someone visited a Myspace profile infected with the script, their profile would also be infected, leading to an rapid spread of his script across the Myspace site.

In this case the code was stored in the database as data and when rendered onto the page turned into code. Known as a stored XSS attack.

Let’s go over the principles that will allow us to develop frontend security XSS spidey senses to help avoid these kinds of attacks.

Developing XSS spidey senses

A good rule of thumb when working on the backend is to never trust anything that comes from the frontend.

Following our “healthy paranoia” principle, a good rule of thumb for the frontend is to never trust user generated data that comes from the backend.

It might sound extreme, but we can’t really know for sure if a user has somehow saved malicious data.

It also means even if it’s escaped properly today on the frontend, there is no guarantee it continues to be escaped down the line as things change and move around.

Whenever we render something to the page, or have to deal with any type of user generated input, it’s good to get those spidey senses going.

The obvious example is any type of form input that gets sent along with a http request to send data to the backend. But it also includes things like using query params in the url, headers, cookies and file and image uploads and the list goes on.

Most of the time our frameworks do a great job of this for us. Despite this there is no shortage of web apps being hacked.

Let’s now take a look at how this plays out when using a framework like React.

React XSS

React prevents a range of XSS attacks by default by escaping the strings it renders.

That’s one of the benefits of using a modern framework like React that abstracts the DOM away. So we have to worry about being being hacked every time we update the DOM.

This means if you’re using the standard patterns of React, you’ll be relatively safe.

But React comes with some escape hatches, and sometimes we have no choice but to use them. For example when we have no choice but to integrate with some other legacy system or library.

In these cases, it’s good to train our spidey sense on how things may go wrong, to prevent the most common types of XSS attacks:

Server side rendering with prefetched data

When server side rendering, a somewhat common pattern is to include pre-fetched data from the backend on the page.

This helps performance by saving the frontend from having to request that data after the client side Javascript has booted up. The data is already available on the page without needing to make a request.

As an example, you may have a super simplified HTML page that looks like:

;`<!doctype html>
    <html>
      <body>
        <!-- html string from React rendered on the server -->
        <div id="root">${html}</div>
        <script>
          window.__INITIAL_DATA = ${JSON.stringify(prefetchData)}
        </script>
        <!-- ... more script tags down here etc -->
      </body>
    </html>`

Spidey senses alert. Remember we can’t really trust user generated data we get from the backend.

In this case we’re outside of the React context when we’re putting stuff from prefetchData on the page.

It’s possible someone managed to save some data like:

{
  name: "Cornelius Fudge",
  description: "</script><script>alert('expelliarmus')</script>"
}

The main things to notice here, is that someone entered malicious html as data. Also that the malicious string starts with a closing </script> tag.

When the browser renders the template above, it will see that as the closing tag to the first <script> where we set __INITIAL_DATA.

The browser will then go on to close that script tag, and render the malicious script tag instead to the page.

In our hand-rolled SSR example, we’re outside the safety of the React framework and so we are vulnerable to to XSS.

A simple fix for this is to encode it using a library like serialize-javascript before rendering it on the page.

Even when inside the context of React, we are still vulnerable to XSS if we are not careful as we’ll see next.

Using dynamic content in JSX attributes

Let’s say we have a simple form that allows users to submit their favorite websites with name and a url properties. Somewhere in our code we then render a list of data:

data.map(({ name, url }) => (
  <Card>
    <a href={url}>{name}</a> // oh shi..
  </Card>
))

This is a very common type of XSS attack. Brought to you by the javascript: attribute that you can put in the href attributes.

In this example a user can add a url that looks like:

<a href="javascript:alert(1)">damn</a>

Their script will run when the link is clicked. The javascript: protocol is is a legacy protocol used on the web that is technically a feature, but resulted in a very common xss exploit.

React decided to keep that behaviour to align with the web. Other frameworks like Angular take a more opinionated stance and don’t allow it at all.

To React’s credit though, a warning does show if you write like this code, saying that it will be removed in future versions of React.

As browsers evolve and change, this highlights the battle between browser capabilities and features, and the potential exploits that can come as a result of those features.

So what’s the best practice to address this?

Back to our “healthy paranoia”, we can’t trust user input. If there is no other way but to render it on the page, so we need to sanitize things.

A naive approach would be to check for the presence of the javascript: protocol and block it out.

However for any security related issues a blocklist approach is asking for trouble. Because we’re tempting someone to find a bypass somehow.

For example an attack could bypass the check for the javascript: protocol using the data: attribute which is also susceptible to the same kind of attack.

It’s usually better to default things like this to an allowlist, that only allow certain things and ignore everything else.

Compared to allowing anything but only certain things ignored, it’s much more robust.

Accessing the DOM directly

More defence against the frontend dark arts

Using a Content Security Policy

Browsers can’t really tell the difference between scripts downloaded from your origin vs another third-party origin.

Or put another way, once the browser has fetched and started processing the scripts, it’s all the same to the browser.

A CSP (content security) policy tells modern browsers which sources they can actually trust. In addition to what types of resources.

You can set specific polices within a CSP, for example only allowing images from a certain domain, or only allowing forms to submit to a specific domain.

CSP prevents our site from making requests to other sites. In the worst case this helps limit the damage an attack can do.

While CSP can provide good protection, configuring CSP is hard to get right and can lead to a false sense of security if not done by someone who really knows the ins and outs of effective policies.

Trusted Types

Trusted types aim to fix the most common root causes of DOM based XSS attacks. It works in combination with CSP to bolster it’s effectiveness against these attacks.

If we recall that most XSS attacks boil down to user inputted data as a string that ends up running as code inadvertently in the browser - when trusted types are enabled, the browser no longer accepts string based inputs to all the common browser API’s that put content on the page. For example APIs like innerHTML, document.write etc.

This in combination with CSP can provide great protection against XSS attacks.

When it’s enabled you have to use new trusted types that form the primitives for rendering content on the page safely.

Trusted types can be enabled through a CSP header or meta tag. This can also be run in report only mode to get a sense of what parts of your code, and your dependencies code have problematic areas.

They require a bit of set up through policies, where much more detail is available on the trusty MDN docs.

Recap

Frontend security is a hard topic, that is incredibly broad. We focussed on the underlying mental models that help us write more secure and resilient code by default.

From there we looked at the most common form of frontend security issue - XSS, and the most common ways it creeps in, even when using modern frameworks like React.

The main takeaway is to not get complacent. Developing a background process of a kind of “positive paranoia” can help with us stay vigilant and help us ask the right questions.

We also want to take advantage of all the security features browsers provide too. In the context of XSS protection, we looked at things like like properly configured CSP policies and newer browser capabilities like Trusted Types that further constrain and eliminate the surface area of XSS vulnerabilities.

We barely scratched the surface. If you’re interested in going deeper there are a number of resources I have found useful listed below.

References

Want to level up?

Get notified when new content comes out

Feel free to unsubscribe anytime. No spam.