Concepts
Frontend Security

Front-end security best practices

Use strong content security policy

CSP (Content Security Policy) is a standard that was introduced in browsers to detect and mitigate certain types of code injection attacks, including cross-site scripting (XSS) and clickjacking. Strong CSP can disable potentially harmful inline code execution and restrict the domains from which external resources are loaded. You can enable CSP by setting Content-Security-Policy header to a list of semicolon-delimited directives. If your website doesn’t need access to any external resources, a good starting value for the header might look like this:

Content-Security-Policy: default-src 'none'; script-src 'self'; img-src 'self'; style-src 'self'; connect-src 'self';

Here we set script-srcimg-srcstyle-src, and connect-src directives to self to indicate that all scripts, images, stylesheets, and fetch calls respectively should be limited to the same origin that the HTML document is served from. Any other CSP directive not mentioned explicitly will fallback to the value specified by default-src directive. We set it to none to indicate that the default behavior is to reject connections to any URL.

However, hardly any web application is self-contained nowadays, so you may want to adjust this header to allow for other trusted domains that you may use, like domains for Google Fonts or AWS S3 buckets for instance, but it’s always better to start with the strictest policy and loosen it later if needed.

Use CORS middleware

Setting up CORS headers (Cross-Origin Resource Sharing) in the server's middleware prevents calls from external sources that can be unknown and potentially dangerous.

For Next.js, you can configure certain CORS headers for the routes in next.config.js like this (this is an example only, please set headers that make sense for your project):

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: "/api/(.*)",
        headers: [
          { key: "Access-Control-Allow-Credentials", value: "true" },
          { key: "Access-Control-Allow-Origin", value: "*" }, // Change this to specific domain for better security
          {
            key: "Access-Control-Allow-Methods",
            value: "GET,OPTIONS,PATCH,DELETE,POST,PUT",
          },
          {
            key: "Access-Control-Allow-Headers",
            value:
              "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
          },
        ],
      },
    ];
  },
};

If you have an Express back-end server that you want to secure, as well, you can use cors npm package to configure CORS middleware in your Express app code. Visit the package's page (opens in a new tab) for code snippets.

Disable iframe embedding to prevent clickjacking attacks

Clickjacking is an attack where the user on website A is tricked into performing some action on website B. To achieve this, malicious user embeds website B into an invisible iframe which is then placed under the unsuspecting user’s cursor on website A, so when the user clicks, or rather thinks they click on the element on website A, they actually click on something on a website B. We can protect against such attacks by providing X-Frame-Options header that prohibits rendering of the website in a frame:

"X-Frame-Options": "DENY"

Alternatively, we can use frame-ancestors CSP directive, which provides a finer degree of control over which parents can or cannot embed the page in an iframe:

Content-Security-Policy: frame-ancestors <source>;

Limit access to browser features & APIs (this is still in experimental phase)

Part of good security practice is restricting access to anything that is not needed for the proper use of our website. We’ve already applied this principle using CSP to limit the number of domains the website is allowed to connect to, but it can also be applied to browser features. We can instruct the browser to deny access to certain features and APIs that our application doesn’t need by using Feature-Policy header (opens in a new tab).

We set Feature-Policy to a string of rules separated by a semicolon, where each rule is the name of the feature, followed by its policy name:

"Feature-Policy": "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none';  picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vr 'none';"

Don’t leak referrer value

When you click on a link that navigates away from your website, the destination website will receive the URL of the last location on your website in a referrer header. That URL may contain sensitive and semi-sensitive data (like session tokens and user IDs), which should never be exposed. To prevent leaking of referrer value, we set Referrer-Policy header to no-referrer:

"Referrer-Policy": "no-referrer"

This value should be good in most cases, but if your application logic requires you to preserve referrer in some cases, check out this article by Scott Helme where he breaks down all possible header values and when to apply them.

Don’t set innerHTML value based on the user input

Cross-site scripting attack in which malicious code gets injected into a website can happen through a number of different DOM APIs, but the most frequently used is innerHTML. You should never set innerHTML based on unfiltered input from the user. Any value that can be directly manipulated by the user -  be that text from an input field, a parameter from URL, or local storage entry - should be escaped and sanitized first. Ideally, use textContent instead of innerHTML to prevent generating HTML output altogether. If you do need to provide rich-text editing to your users, use well-established libraries that use whitelisting instead of blacklisting to specify allowed HTML tags. Unfortunately, innerHTML is not the only weak point in DOM API, and the code susceptible to XSS injections can still be hard to detect. This is why it is important to always have a strict content security policy that disallows inline code execution. For the future, you may want to keep an eye on a new Trusted Types specification which aims to prevent all DOM-based cross-site scripting attacks.

React provides some protection against script injection attacks out of the box, such as:

  • Auto escaping data inside elements
  • Cautionary name for dangerouslySetInnerHTML operation as a reminder that you should avoid it

Tips!

  1. Sanitize your data input with libraries like dompurify (opens in a new tab) before you proceed to render it inside your React components.

  2. If you need to modify text content inside a specific DOM element (for example, you are using a useRef() hook to get to it), use innerText prop to mutate the element:

import './App.css';
import {useEffect, createRef} from 'react';

function App() {
  const divRef=createRef();
  const data="lorem ipsum just some random text"

  useEffect(()=>{
    divRef.current.innerHTML="After rendering, this will display"
  },[])

  return (
    <div className="App">
      <div className="container" ref={divRef}>
        {data}
      </div>
    </div>
  );
}

Examples are taken from this article (opens in a new tab).

Keep your dependencies up to date

A quick look inside node_modules folder will confirm that our web applications are lego puzzles built out of hundreds (if not thousands) dependencies. Ensuring that these dependencies don’t contain any known security vulnerabilities is very important for the overall security of your website. The best way to make sure that dependencies stay secure and up-to-date is to make vulnerability checking a part of the development process. To do that, you can, for example:

  • run npm audit regularly to get a report of known vulnerabilities among your dependencies.
  • integrate tools like Dependabot (opens in a new tab) and Snyk (opens in a new tab), which will create pull requests for out-of-date or potentially vulnerable dependencies and help you apply fixes sooner.

Think twice before adding third-party services

Third-party services like Google Analytics, Intercom, Mixpanel, and a hundred others can provide a “one line of code” solution to your business needs. At the same time, they can make your website more vulnerable, because if a third-party service gets compromised, then so will be your website. Should you decide to integrate a third-party service, make sure to set the strongest CSP policy that would still permit that service to work normally. Most of the popular services have documented what CSP directives they require, so make sure to follow their guidelines. Especial care should be taken when using Google Tag Manager, Segment, or any other tools that allow anyone in your organization to integrate more third-party services. People with access to this tool must understand the security implication of connecting additional services and ideally discuss it with their development team.

It’s worth clarifying that this technique is useful for third-party libraries, but to a lesser degree for third-party services. Most of the time, when you add a script for a third-party service, that script is only used to load another dependant script. It’s not possible to check the integrity of the dependant script because it can be modified at any time, so in this case, we have to fall back on a strict content security policy.

How to set security headers in Next.js

Here's a universal code snippet to use when you want to introduce any security headers (CORS, Feature-policy, X-Frame, etc.) in your server responses:

// next.config.js

// You can choose which headers to add to the list
// after learning more below.
const securityHeaders = [

    // here's a sample header
    {
        key: "X-Frame",
        value: "DENY",
    }
]

module.exports = {
  async headers() {
    return [
      {
        // Apply these headers to all routes in your application.
        source: '/:path*',
        headers: securityHeaders,
      },
    ]
  },
}

Use Subresource Integrity for third-party scripts

For all third-party scripts that you use, make sure to include integrity attribute when possible. Browsers have Subresource Integrity feature (opens in a new tab) that can validate the cryptographic hash of the script that you’re loading and make sure that it hasn’t been tampered with. This how your script tag may look like:

<script src="https://example.com/example-framework.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
        crossorigin="anonymous"></script>