Front-end Security Hanbook

Tuan Tran VanTuan Tran Van
40 min read

When it comes to security, front-end security is a crucial aspect of web development that is often overshadowed by its back-end counterpart. However, overlooking front-end security can leave your web applications vulnerable to a wide range of threats, including cross-site scripting (XSS) attacks, cross-site request forgery (CSRF) attacks, and other security vulnerabilities. This article will expose essential front-end security best practices to help you, the front-end developer, safeguard your web applications from malicious scripts and potential security risks.

Understanding the front-end security landscape

Before delving into best practices, let’s establish a foundational understanding of front-end security and the associated terminology. Front-end security primarily deals with protecting the client-side of web applications, including the user interface and any Javascript code executed in the user’s browser. It focuses on mitigating security risks by implementing various security measures.

Maintaining a cybersecurity mindset in front-end development is crucial because it emphasizes the importance of proactive security measures. Front-end developers need to be vigilant in preventing common vulnerabilities like cross-site scripting (XSS) and ensuring data remains protected. It means following best practices, staying updated on threats, and thinking like a potential attacker to identify and fix vulnerabilities before they become problems. This mindset helps create secure web applications that prioritize user privacy and safety right from the start.

Common Issues With Front-End Security

There are countless issues that linked to Front-end security that could cause significant issues to the entire application. However, the following are some of the most prevalent problems that must be considered when considering Front-end security:

Cross-Site Scripting (XSS)

As web development evolves, the risk associated with protecting web applications is also increasing. One of the most significant and growing threats today is Cross-Site Scripting (XSS). The short form of Cross-site scripting is css, but this naming will conflict with Cascading Style Sheets (CSS). So, we call cross-site scripting as XSS.

XSS is a type of security vulnerability where attackers inject malicious scripts into web pages viewed by users. Since the malicious code is injected on the client side, the victims unknowingly execute the code, allowing attackers to bypass access controls and gain unauthorized access to sensitive information.

Improving protection against XSS is crucial in maintaining the security and integrity of web applications. Developers must implement robust security measures, such as input validation, output encoding, and using security-focused libraries, to mitigate the risks associated with CSS attacks.

Understanding the Risks of XSS

XSS attacks can have several consequences, including:

  • User data being compromised: Sensitive user information such as personal details, passwords, and financial data can be exposed to attackers.

  • Stealing of cookies: Cookies, which are small pieces of data sent from a server to a web browser containing user information, can be stolen by attackers. This can lead to session hijacking and unauthorized access to user accounts.

  • Unauthorized actions performed on behalf of the user: Attackers can perform actions such as making transactions, changing account settings, or sending messages without the user’s consent.

  • Capturing the keystrokes of the user: By injecting malicious scrips, attackers can monitor and record user keystrokes, leading to the theft of sensitive information like passwords and credit card numbers.

Decoding XSS Attack Mechanisms

DOM-based XSS is a common type of vulnerability where an attacker exploits the Document Object Model (DOM) to execute malicious script into the user’s browser. This occurs when client-side scripts directly manipulate the DOM, leading to unexpected execution of the injected scripts.

Consider the following example, where a web page uses Javascript to read the URL hash fragment and display it on the screen.

<!DOCTYPE html>
<html>
<head>
    <title>DOM XSS Example</title>
</head>
<body>
    <h1>Welcome!</h1>
    <p id="hashElement "></p>

    <script>
        // Get the hash value from the URL
        var hash = window.location.hash.substring(1);

        // Display the hash value in the " hashElement" 
        document.getElementById(“hashElement”).innerHTML = "Hello, " + hash + "!";
    </script>
</body>
</html>

In the above code snippet, the script retrieves the hash fragment from the URL and directly inserts it into the DOM. However, this approach is insecure. An attacker can exploit this by crafting a URL such as:

http://example.com/#<script>alert('XSS')</script>

When a user visits a page with a URL containing a malicious hash fragment like the one above, the script executes immediately upon the page load. This vulnerability arises because the Javascript code uses innerHTML to insert the unsanitized hash fragment into the DOM.

If, instead, a simple alert, the injected script contains malicious code designed to steal sensitive information, the consequences could be severe. For example, an attacker could craft a URL like http://example.com/#<script>stealCookies()</script>, where stealCookies() is a function that sends the user’s cookie to the attacker’s server, and then the user's sensitive information is compromised.

Mitigation Strategy for XSS Vulnerabilities

This kind of attack can be prevented if the user input is sanitized and validated before it’s inserted into the DOM.

  • Prefer using APIs that inherently do not interpret HTML, such as textContentinstead of innerHtml.

document.getElementById('hashElement').textContent = "Hello, " + hash + "!";

  • Sanitize Input: Use libraries like DOMPurify to clean user input before inserting it into the DOM.

var cleanHash = DOMPurify.sanitize(hash);
document.getElementById('hashElement').innerHTML = "Hello, " + cleanHash + "!";

React’s Approach to Mitigating XSS Vulnerabilities

React, a popular Javascript library for building user interfaces, has built-in mechanisms to help prevent DOM-based XSS attacks.

React will automatically escape all the content that is embedded inside the JSX, so that it will treat the content inside JSX as a plain instead of HTML thereby preventing the insertion of any external scripts.

import React from 'react';

class SampleComponent extends React.Component {
  render() {
    // Assume the hash value comes from the URL and is passed as a prop
    const { hash } = this.props;

    return (
      <div>
        <h1>Welcome!</h1>
        <p>Hello, {hash}!</p>
      </div>
    );
  }
}

export default SampleComponent;

In the above example, even if the hash variable contains the script tag, React will escape it and render it as a string and not as executable code.

Using dangerouslySetInnerHtml

React offers the dangerouslySetInnerHtml attribute as a method to directly inject HTML into the DOM. However, it should be used cautiously, and only you can guarantee the content is safe and thoroughly sanitized.

If you encounter unavoidable scenarios where using the dangerouslySetInnerHtml attribute is necessary, always ensure that the input is sanitized rigorously with a trusted library like DOMPurify. This step helps remove any potentially harmful code, thereby mitigating the risk of XSS vulnerabilities.

import DOMPurify from 'dompurify';

class XSSComponent extends React.Component {
  render() {
    const { hash } = this.props;
    const sanitizedHash = DOMPurify.sanitize(hash);

    return (
      <div>
        <h1>Welcome!</h1>
        <p dangerouslySetInnerHTML={{ __html: `Hello, ${sanitizedHash}!` }} />
      </div>
    );
  }
}

export default XSSComponent;

Cross-Site Request Forgery (CSRF)

Cross-site request forgery (also known as CSRF) is a web security vulnerability that allows an attacker to introduce users to perform actions that they do not intend to perform. It allows an attacker to partly circumvent the same-origin policy, which is designed to prevent different websites from interfering with each other.

For example, Chris might be logged into his online banking while checking his email. In the meanwhile, he might click on a link in the phishing email with a transfer request (such as a deceptively short link) asking Chris’s bank to transfer money to the compromised bank account. Since Chris is logged into his bank account, the transfer request will be executed automatically because it is made through a browser authorised by Chris.

To prevent the CSRF in your web application, this is the step we should follow:

  1. Store the token under HttpOnly with sam-site Cookie Attribute

Generally, we don’t save the user token inside local storage or redux. So far, the suggestion that I have found is to save the cookie inside HttpOnly cookies with strict same-site configuration. Well, the HttpOnly cookies mean that the token can not be loaded on the client-side; it is a tag added to the browser cookie that prevents client-side scripts from accessing data. It provides a gate that prevents the specialized cookie from being accessed by anything other than the server. Nextjs would be a good way to achieve this solution because it has an express backend server as a developer that is able to save tokens inside HttpOnly cookies. Besides that, the cookies should be set to strict because the browser will not include the cookie in any requests that originate from another site. This is the most defensive option, but it can impair the user experience because if a logged-in user follows a third-party link to a site, then they will appear not to be logged in and will need to log in again before interacting with the site in the normal way.

Reference link: https://portswigger.net/web-security/csrf/samesite-cookies

  1. Follow REST principles

It is very dangerous if we use this GET method to change the state of the server. For example, If Chris is accessing the bank websites, and the bank website uses a GET request to transfer funds as example below - GET <http://bank.com/transfer.do?name=Chris&amount=10> HTTP/1.1 This is very dangerous if the attacker tamper with the query strings and make a link into an image or a script and send it to the user via an unsolicited email with HTML content, then Chris’s bank account will automating executed that query while they are doing the online banking. The attackers can even put the script such as <img src="<http://bank.com/transfer?name=Chris&amount=10>" width="0" height="0" border="0"> . Other methods such as Create , Update , Delete have the same issue with GET method. So the best way to prevent as a developer is to follow REST principles as they are provided.

  1. Implement Anti-CSRF tokens

Anti-CSRF tokens are to provide the user browser with a piece of information and check if the web browser will send it back. The token must be unique and provided by the third party. After that, the back end of the web application will not proceed unless it verifies the token. In this way, only the user can send requests within an authenticated session. Below is an example:

<Form>
  <input type="name" />
  <input type="hidden" name="token" value="unique_value" />
</Form>

If the attacker performs a CSRF using a malicious site, it will be hard to know the current token that is set inside the cookies. The backend server won’t process the request without that token in order to make a protection from CSRF attacks. More references can read this:

Reference link: Protect your website with anti-CSRF tokens | Invicti

Injection Attack

As the name implies, injection attacks or vulnerabilities allow an attacker to inject malicious code or commands into the application’s input fields, exploiting vulnerabilities in data processing mechanisms.

This is one of the most common forms of vulnerability that can be present in an application and has also been added to the OSWAP Top 10 list of vulnerabilities.

Even though there can be several injection attacks, such as SQL, Command, or even XPath injections, the principle behind the vulnerability remains the same.

Some of the most common causes for the injection vulnerabilities include:

  • Lack of input validation: Failure to properly verify and sanitize user input before processing allows attackers to introduce harmful payloads into application input fields.

  • Dynamic Query Construction: Applications that dynamically generate SQL queries, shell commands, or XPath expressions from user inputs are particularly vulnerable to injection attacks.

  • Insufficient Escaping: Insecure handling of special characters or inability to escape the user input before combining it into queries or instructions opens programs to injection attacks.

How can you prevent Injection Attacks?

Preventing an injection attack is a two-part strategy:

  1. First, you need to ensure that your Front-end input fields are rightfully validated and sanitized. You need to prevent users from inserting malicious code into your fields.

  2. After you have validated your front end, it’s also important to sanitize the payloads you receive on your backend. Don’t trust your payloads that anyone can get your API endpoints and start sending malicious input. So, ensure that you sanitize your back-end payloads as well. Additionally, utilize tools like Burp Scanner, sqlmap, jSQL Injection, and Invicti to detect potential SQL attacks and related vulnerabilities in your applications.

Man-in-the-Middle Attack

Man-in-the-middle (MitM) Attacks force an attacker to intercept and manipulate the information that is being transmitted between two parties.

For example, attackers can intercept your connection to Facbook.com and steal your credentials and forward you back to Facebook.

These attacks occur when an attacker exploits an insecure communication channel (often through public wifi). A victim of this attack doesn’t feel that they are being attacked as they assume they are holding a perfectly normal and secure conversation with the server when the information they are sharing is being spied upon or altered along the way.

How can you prevent a Man-in-the-middle attack?

  • Use a secure internet connection and log out of apps you no longer use.

  • Don’t connect to networks you don’t know. For example, don’t connect to a free Wifi that is available in your coffee.

  • Use secure communication protocols such as HTTPS and TLS to encrypt all data in transit.

Clickjacking

Clickjacking, also known as UI redressing, is a type of attack where malicious actors tricks users into clicking on something different from what they perceive by embedding web pages within iframes. This can lead to unauthorized actions and compromise user security.

Clickjacking involves placing a transparent or opaque over a legitimate webpage element, causing users to unknowingly perform actions such as changing settings or transferring funds.

Consider a scenario where an attacker embeds a hidden iframe from a banking site into a trusted webpage. When a user clicks on a seemingly harmless button, they might actually be authorizing a bank transaction.

Here’s an example of a vulnerable page:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Clickjacking Example</title>
</head>
<body>
    <h1>Welcome to Our Site</h1>
    <button onclick="alert('Clicked!')">Click Me</button>
    <iframe src="https://example-bank.com/transfer" style="opacity:0; position:absolute; top:0; left:0; width:100%; height:100%;"></iframe>
</body>
</html>

Preventing Clickjacking with Javascript

To prevent clickjacking attacks, you can use JavaScript to ensure that your website is not being framed. Here’s a step-by-step guide on how to implement this protection:

  1. Javascript Fram Busting

Frame busting involves using JavaScript to detect if your website is loaded inside an iframe and breaking out of it.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Frame Busting Example</title>
    <script>
        if (window.top !== window.self) {
            window.top.location = window.self.location;
        }
    </script>
</head>
<body>
    <h1>Secure Site</h1>
    <p>This site is protected from clickjacking attacks.</p>
</body>
</html>

In this example, the JavaScript checks if the current window (window.self) is not the topmost window (window.top). If it's not, it redirects the topmost window to the current window's URL, effectively breaking out of the iframe.

  1. Enhanced Frame Busting with Event Listeners

You can further enhance your frame-busting technique by using event listeners to continuously check if your page is framed.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Enhanced Frame Busting</title>
    <script>
        function preventClickjacking() {
            if (window.top !== window.self) {
                window.top.location = window.self.location;
            }
        }

        window.addEventListener('DOMContentLoaded', preventClickjacking);
        window.addEventListener('load', preventClickjacking);
        window.addEventListener('resize', preventClickjacking);
    </script>
</head>
<body>
    <h1>Secure Site</h1>
    <p>This site is protected from clickjacking attacks.</p>
</body>
</html>

In this example, the preventClickjacking function is called on the DOMContentLoaded, load, and resize events to ensure continuous protection.

Server-Side Protection

While JavaScript methods are useful, implementing server-side protection provides an additional layer of security. Here’s how to set up HTTP headers in Apache and Nginx to prevent clickjacking:

  1. X-Frame-Options Header

The X-Frame-Options header allows you to specify whether your site can be embedded in iframes. There are three options:

DENY: Prevents any domain from embedding your page.
SAMEORIGIN: Allows embedding only from the same origin.
ALLOW-FROM uri: Allows embedding from the specified URI.
Example:

X-Frame-Options: DENY

Apache Configuration
Add this header to your server configuration:

# Apache
Header always set X-Frame-Options "DENY"

Nginx Configuration
Add this header to your server configuration:

  1. Content-Security-Policy (CSP) Frame Ancestors

CSP provides a more flexible approach through the frame-ancestors directive, which specifies valid parents that may embed the page using iframes.

Example:

Content-Security-Policy: frame-ancestors 'self'

Apache Configuration
Add this header to your server configuration:

Example:

# Apache
Header always set Content-Security-Policy "frame-ancestors 'self'"

Nginx Configuration
Add this header to your server configuration:

# Nginx
add_header Content-Security-Policy "frame-ancestors 'self'";

Insecure Direct Object Reference (IDOR)

This type of vulnerability occurs when the application exposes the internal object references in a predictable or unauthenticated manner, allowing attackers to manipulate these references to gain unauthorized access to sensitive data or resources.

IDOR works when internal object references, such as database keys or file paths, are directly exposed to users without proper authorization checks. This way, attackers can access these resources by guessing or incrementing values to access these resources.

Common causes of IDOR vulnerability include:

  1. Lack of Access Controls: Failure to implement sufficient access controls or permission procedures allows users to access internal object references without suitable validation directly.

  2. Predictable Object References: Applications with predictable or sequential object references, such as sequential database keys or predictable file paths, are more vulnerable to IDOR attacks.

  3. Insecure Direct Links: Direct links or URLs that expose internal object references without adequate authentication or authorization might result in IDOR vulnerabilities.

Insecure Authentication and Session Management

These vulnerabilities allow attacks to bypass or masquerade valid sessions and users into gaining unauthorized access to sensitive resources.

This type of vulnerability has been persistent for a long time and has also been mentioned within the OWASP Top 10 list of vulnerabilities and on the OWASP Top 10 for APIs.

Some common causes for these vulnerabilities include:

  • Session Fixation: Improper management of session IDs, such as failing to regenerate session tokens after authentication or utilizing predictable session identifiers, can expose the application to session fixation attacks. Attackers can take over user sessions by altering or guessing session tokens.

  • Persistent Cookies: Persistent cookies without expiration dates or long periods raise the possibility of unauthorized access and account compromise. Attackers can steal the persistent session cookie saved on users’ devices and exploit it to get continued access to their accounts.

  • Insufficient Account Lock Mechanisms: A lack of account logout methods and insufficient rate limits on login attempts might make user accounts vulnerable to brute-force assaults. Attackers can keep guessing passwords until they gain unauthorized access to user accounts.

Thrid-Party Component Risks

Almost all modern-day applications use third-party components such as libraries, frameworks, plugins, and APIs to accelerate development and enhance functionality. Even though these components have their benefits, they can also introduce inherent security risks that could jeopardize the application’s security and integrity.

Some of the most common risks brought in by third-party components are:

  1. Security Vulnerabilities: Third-party components may have security flaws or authentication bypasses that attackers might use to compromise your application.

  2. Outdated or Unsupported Versions: Using obsolete or unsupported versions of third-party components increases the risk of security vulnerabilities, as patches and updates that address known vulnerabilities may not be applied.

  3. Supply Chain Attacks: Attackers may compromise the software supply chain by injecting malicious code or backdoors into third-party components, resulting in widespread security breaches or data exfiltration.

Malicious File Upload

File uploads could be risky when the developer doesn’t restrict the file upload. The attacks could be to the backend infrastructures and users. Let’s say the uploaded file contains malware, which can leverage the vulnerability on the server side. The file could be used to gain control of the server. Back-end developers should restrict and scan the file uploading. But in the front end, we could try to optimize it to prevent malicious file uploads. We are unable to scan the file on the server. However, we can run some validations from the recommendations below.

  • only allows file extensions that your application requires

  • If the file extensions are valid, then check the type of the file. e.g, application/text, application/csv, etc.

  • Upload should be done over the secure channel (SSL)

  • You can get an antivirus/malware detector in your hosting services.

  • Proper permissions to the folder when you move new files.

  • Confirm that the file names are standard and without any special characters / Randomize upload file names.

  • Set a maximum name length and maximum file size.

Reference Link: Front-end Untivirus Scan File Uploads

Denial of Service (DoS)

Think of a busy road suddenly blocked by hundreds of cars, making it impossible for anyone to get through. That’s what a Denial of Service (DoS) attack does in the digital world. It clogs up websites or online services, so they become inaccessible to users.

In a DoS attack, cyber troublemakers flood a website or a service with an overwhelming amount of traffic or data. It’s like sending so many cars onto a road that it becomes jammed. When this happens, the website or service can’t handle all the requests, and it crashes or slows down significantly.

These attacks can be launched for several reasons. Sometimes, it’s to cause chaos and disrupt a service, but other times, it’s a distraction while cybercriminals carry out other attacks.

To protect against DoS attacks, website owners and service providers use specialized software and hardware to filter out malicious traffic. They also have backup systems to keep services running even if there is an attack.

As a user, you might experience a website's response slowly during a DoS attack, but there’s not much you can do to prevent it. Just like dealing with traffic jams on the road, patience is key when facing a DoS attack online.

Distributed Denial of Service (DDoS)

Imagine your favorite online game or a popular shopping website suddenly becoming so crowded that it crashes, and you can’t access it. That is what a Distributed Denial of Service (DDoS)attack does - it creates a digital stampede that overwhelms and paralyzes websites and online services.

In a DDoS attack, instead of one troublemaker, there are many. These cyber attackers gather a network of hijacked computers and devices, often called a “botnet“. It’s like an enemy of digital zombies that follows the hacker’s orders.

When the attack begins, the botnet floods the target website or service with a massive amount of fake traffic. It’s like thousands of people trying to get into a tiny shop at once. The target gets so swamped that it can’t handle all the requests, and it slows down or crashes.

DDoS attacks can be used for several reasons, from causing chaos and distracting from security teams while another cyber-attack is underway.

To protect against DDoS attacks, websites and service providers invest in strong cybersecurity infrastructure and monitoring systems to detect and mitigate the attack traffic.

As users, there is not much you can do to prevent a DDoS attack, but you can be patient and wait for the storm to pass. Just like waiting for a crowd event to calm down, staying calm during a DDoS attack is key to getting back online.

Front-end security best practices

Now that we have a clear picture of the threats, let’s explore some best practices to fortify your Front-end security.

CORS Finally Explained

Seen the above before? Probably… and probably quite a lot…

There are millions of articles explaining how to fix the error above, but what exactly is this “Cross-Origin Resource Sharing“ thing, and why does it even exist?

Imagine this: you log into bank.com, which is your banking service. After logging in, a “session cookie is stored in your browser“. (Session cookies basically tell the server behind bank.com that your browser is now logged into your account.) All feature requests to bank.com will now contain this cookie, and it can respond properly, knowing you are logged in. Ok, so now you decide to check your mailbox. You receive a suspicious email, and of course, you choose to click on the link inside, which sends you to attack.com. Next, this website sends a request to bank.com to get your banking details. Keep in mind that bank.com still thinks that you are still logged in because of that session cookie…that cookie is stored in your browser. For the server behinds bank.com, it just looks like you requested your banking details normally, so it sends them back. Now, attack.com has access to these and store them elsewhere maliciously.

People realized this was bad, so browsers adopted an SOP (Same-Origin Policy) where if your browser notices that you are trying to make requests to bank.com from anywhere other than bank.com they will be blocked. Now, this is a key thing to realize - this is a browser-based policy. bank.com really has no way to tell where a request comes from, so it can’t protect much against attacks like CSRF. The browser you are using steps in and basically says that if you seem to be trying to request details for an origin (scheme + domain name + port, https//foo.com:4000, http//bar.org:3000, etc… Basically the URL), it will only send those requests for same origins.

Now, this was great at all, but it was incredibly limiting. I mean, public APIs won’t work at all. You couldn’t request data from the unless you used some sort of proxy solution.

CSRF

Here’s a thing: a server can sorta tell where a request came from. There is an “Origin“ header, which requests should have, showcasing what origin made a request. For instance, in the above example, the request would look something like this:

Request to -----> bank.com
{
  Headers: { Origin: http://attack.com }
}

bank.com in theory, should be checking this to make sure it only responds to requests where the origin makes sense. And it usually does, so SOP seems kinda limiting.

This is where CORS comes in.

CORS

When a web application from example.com attempts to request resources from bank.com, the browser automatically includes an Origin header in the request, indicating where the request originates from (example.com). Here's the crucial part: Instead of outright blocking such cross-origin requests under the SOP, bank.com's server can inspect this Origin header and decide whether to allow or deny the request based on its own CORS policy.

If bank.com considers example.com trustworthy or the resource being requested is meant to be publicly accessible, it can respond with specific CORS headers, such as Access-Control-Allow-Origin, indicating which origins are permitted to access the resource. This header might be set to http://example.com, explicitly allowing this origin, or * for public resources that any origin can access.

Of course, the browser facilitates all this. If any of it is wrong, you will get that nasty error.

Now, what if the request doesn’t have the origin header? What if it has a bunch of other headers and doesn't use one of the basic HTTP methods?

In these situations, the handling of CORS becomes a bit more intricate as it is no longer a “simple request“. This is where the concept of “preflight” requests in CORS comes into play.

Preflight

For certain types of requests that could potentially modify data on the server — Those are use HTTP methods like PUT, DELETE, or use headers that are not automatically included in every requests — browsers will first send a “prefight“ request before making the actual request. This preflight request is an HTTP OPTIONS request, and its purpose is to check with the server whether the actual request is safe to send.

The preflight request includes headers that describe the HTTP method and headers of the actual request. Here’s what happens next:

  1. Server Response: If the server supports the CORS policy and the actual request, it responds to the preflight request with headers indicating what methods and headers are allowed. This might include headers like ccess-Control-Allow-Methods and Access-Control-Allow-Headers.

  2. Browser Decision: Based on the server’s response to the preflight request, the browser decides whether to proceed with the actual request. If the server’s response indicates that the request is allowed, the browser sends it; if not, the browser blocks the request, and you will see an error related to CORS.

Content Security Policies

Content Security Policy (CSP) is an added layer of security that helps to reduce and mitigate certain types of attacks, including cross-site scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement to distribution of malware.

As the name suggests, CSP is a set of instructions you can send with your Javascript code to the browser to control its execution. For example, you can set up a CSP to restrict the execution of Javascript to a set of whitelisted domains and ignore any inline scripts and event handlers to protect from XSS attacks. In addition, you can specify that all the scripts should load via HTTPS to reduce the risk of packet sniffing attacks.

So how should I configure a CSP for web applications?

There are two ways to configure a CSP. One approach is to return a specific HTTP Header Content-Security-Policy. The other is to specify the <meta> element in your HTML page.

Recommended to read this article:

https://www.stackhawk.com/blog/react-content-security-policy-guide-what-it-is-and-how-to-enable-it/

Other than that, NextJs is also able to improve security by setting the headers in next.config.js . More information - Advanced Features: Security Headers | Next.js

Let’s look at a Sample CSP

Assume that you set the following Content-Security-Policy for your web application:

Content-Security-Policy: default-src 'self'; script-src 'self' https://example.com;

Then, this will allow loading Javascript like from https://example.com/js/* but will block https://someotherexample.com/js/* by the browser as specified in the CSP. Furthermore, any Inline Scripts are also blocked by default unless you use hashes and nonces to allow them to execute.

If you haven’t heard of hashes and nonces, I would highly recommend referring to this example link to realize its true potential.

In a nutshell, with the hash operation, you can specify the hash of the Javascript file as an attribute to the script block where the browser will first validate the hash before executing it.

The same goes for the nonce, where we can generate a random number and specify it at the CSP header while referring to the same nonce at the Script block.

How do you know if there are any CSP Violations?

The beauty of CSP is that it has covered the scenario of reporting the violations back to you. As a part of CSP, you can define a URL where the user’s browser will automatically send a violation report back to your server for further analysis.

For this, you need to set up an endpoint that handles the POST payloads sent as the CSP violation report by the browser. The following shows an example of specifying a /csp-incident-reports path to receive a violation report payload.

Content-Security-Policy: default-src 'self'; report-uri /csp-incident-reports

If we include a Javascript or Style outside the site’s own origin (e.g, otherdomain.com), let’s say by accident when we visit a site URL (e.g, example.com), the above policy will reject it from loading to the browser and submit the following violation report as the HTTP POST payload.

{
  "csp-report": {
    "document-uri": "http://example.com/index.html",
    "referrer": "",
    "blocked-uri": "http://otherdomain.com/css/style.css",
    "violated-directive": "default-src 'self'",
    "original-policy": "default-src 'self'; report-uri /csp-incident-reports"
  }
}

Browser Support and Tools

As you can see, all the major browsers support the CSP feature. This is good news since the investment we put in creating the CSP addresses a larger user base.

Besides, browsers like Chrome have gone even further by providing tools to validate CSP attributes. For example, if you define the hash of the Javascript, the Chrome Developer Console will promote the correct hash for developers to rectify any errors.

In addition, if you use Webpack to bundle your Javascripts, you can find a webpack plugin named CSP HTML WebPack Plugin to append the hash at build time and automate this process.

Avoid Iframes if possible

Iframe is a simple content embedding technique used in web development. However, using iframes would come with security risks. Through IFrame, attackers are able to inject malicious executables or viruses into our web application and execute them on the user’s browser. More information can be read this:

Strict User Input (the First Point of Attack)

User Input should always be strict in nature to avoid vulnerabilities such as SQL injection, clickjacking, etc. So it’s important to validate or sanitize user input before sending it to the back end.

Sanitizing data can be done by removing or replacing contextually dangerous characters, such as by using a whitelist and escaping the input data.

However, I realize that sanitizing and encoding is not an easy task for all existing possibilities, so we may use the following open-source libraries:

  • DOMPurify: This is the most simple to use and has one method to sanitize the user’s input. It has an option to customize the rules, and it supports HTML5, SVG, and MathML.

  • secure-filters: A Salesforce library that provides methods to sanitize HTML, Javascript, Inline CSS styles, and other contexts. It’s especially useful when you want to make use of user input in other places, for example, generating CSS or Javascript.

In the case of file upload, always check the file type, use a file filter function, and allow only certain file types to get uploaded. Refer to this for more.

Beware of Hidden Fields or Data Stored in Browser Mermory

If we add input=”hidden” to hide the sensitive data in the page or add them in the browser localStorage, sessionStorage, cookies and think that is safe, we need to think again.

Everything is added to the browser and can be accessed by attackers easily. An attacker can open the dev tools and change all the in-memory variables. And what if you had hidden the auth page upon the localStorage, sessionStorage, and cookies values?

There are tools like ZapProxy and even inspection tools in the browser that can expose those values to attackers if they find a way to inject a script, and they can use them to attack further.

Hence avoid using type="hidden" and avoid storing keys, auth tokens, etc, in the browser's in-memory storage as much as possible.

Enable XSS Protection Mode

If somehow an attacker injects the malicious code from the user input, we can instruct the browser to block the response by supplying the "X-XSS-Protection": "1; mode=block" header.

Most modern browsers have XSS protection mode enabled by default but it’s still recommended to include the X-XSS-Protection header. This helps to ensure better security for older browsers that don’t support CSP headers.

Avoid Typical XSS Mistakes

An XSS attack is usually traced to the DOM API’s innerHTML. For instance:

document.querySelector('.tagline').innerHTML = nameFromQueryString

Any attacker is able to inject malicious code with the line above:

Consider using textContent instead of innerHTML to prevent generating HTML output altogether. If you don’t generate HTML, there is no way to insert Javascript. You may see the content, but nothing will happen.

Keep an eye out for a new Trusted Types specification, which aims to prevent all DOM-based cross-site scripting attacks made by goodgler.

In the case of react.js, dangerouslySetInnerHTML is unambiguous and cautionary and can have a similar impact as innerHTML.

Note: Don't Set the innerHTML value-based on user input and use textContent instead of innerHTML as much as possible.

Also, HTTP response headers Content-Type and X-Content-Type-Options should be set properly, with their intended behavior. For example, JSON data should never be encoded as text/HTML to prevent accidental execution.

Keep Errors Generic

An error like “Your password is incorrect“, may be helpful to the users but also to the attackers. They may figure out information from these errors that helps them to plan their next action.

When dealing with accounts, emails, and PII, we should try to use ambiguous errors like “Incorrect login information.“

Use Captcha

Using Captcha at public-facing endpoints (login, registration, contact). A Captcha is a computer program or system intended to distinguish humans from bots and can help stop DoS (Denial of Service) attacks.

Always Set Referer-Policy

Whenever we use an anchor tag or a link that navigates away from the website, make sure you use a header policy "Referrer-Policy": "no-referrer" or, in case of the anchor tag, set rel = noopener or noreferrer.

When we don’t set these headers and rel, the destination website can obtain data like session tokens and database IDs.

Audit Dependencies Regularly

Run npm audit regularly to get a list of vulnerable packages and upgrade them to avoid security issues.

Github now flags vulnerable dependencies. We can also use Synk, which checks your source code automatically and opens pull requests to bump versions.

Carefully Consider Autofill Fields

Personal identification information stored in the autofill of a browser can be convenient for both users and attackers.

Attackers add third-party scripts to exploit browsers’s built-in autofill to extract email addresses for building tracking identifiers. They can use these to build user browsing history profiles, which they can sell to the bad guys. Read more on this.

Many of us aren’t even aware of what information our browser’s autofill has stored.

Tip: Disable auto-filled forms for sensitive data.

Secure HTTP requests

When making HTTP requests, ensure they are secure by using HTTPS. HTTPS uses TLS to encrypt HTTP traffic, improving safety and security.

So, why is using HTTPS so important? First of all, using HTTPS is more secure for both the user and the web server, as the encryption goes both ways: from the server to the end user and vice versa. This way, none of the information transmitted during the connection is in a plain text, preventing attacks such as man-in-the-middle. Secondly, an SSL certificate authenticates the website, meaning that a trustworthy third party verifies that the web server is who it is claimed to be, protecting the user against threats like website proofing.

These days, using HTTPS is a must in order to gain the trust of potential customers for your website, ensuring basic security through an encrypted connection. It is important to mention that any website without a valid SSL certificate is automatically flagged as “unsecure“.

Penetration Testing

Web applications are critical systems as they are directly exposed to the outside world. Given this criticality, you should be worried about how secure your network is. The only way to truly know is by putting it to the test.

The practice of penetration testing proves to be a valuable asset in detecting vulnerabilities before they can be exploited by malicious actors. By thoroughly evaluating the security measures in place, potential weaknesses can be identified and addressed, reducing the risk of successful cyber attacks. The proactive approach to security reinforces the importance of maintaining a comprehensive and robust security strategy.

During a vulnerability assessment, some crucial types of attacks are being tested for:

  • Injection attacks

  • Broken access control

  • Improper error handling

  • Broken authentication

  • XSS attacks

Sometimes, the people who create your applications may make mistakes. To ensure that, your team’s work is error-free, you can ask an external partner to conduct a penetration test. This test helps to identify and fix any weak spots that could be exploited by hackers. In applications that must comply with PCI DSS or HIPAA regulations, penetration testing is mandatory. It’s considered one of the most effective ways to protect your network from hacking attempts.

Implementing a Web Application Firewall (WAF)

A Web Application Firewall (WAF) is a security device that monitors and filters HTTP traffic to and from a web application. It can be used to protect against various types of attacks, including SQL injection, cross-site scripting (XSS), and cross-site request forgery (CSRF).

You can set up a WAF in front of your React application to protect it from attacks. The WAF will inspect all incoming requests and block any that are deemed malicious. It can also be configured to block requests that contain a specific pattern or match a set of rules.

For example, you can configure the WAF to block requests containing SQL injections or XSS attacks. You can also configure it to block requests containing CSRF tokens stolen from other websites.

A WAF can be implemented in three ways:

  • As a hardware appliance - This is the most expensive option but provides the best performance and security

  • As a software application - This is a cheaper option, but it requires more maintenance and configuration

  • As a cloud service - This is the cheapest option, but it provides the least amount of control over the WAF.

Using Linter plugins and code analysis tools

Linters are tools that analyze your source code to flag programming errors, bugs, stylistic errors, and suspicious constructs. They can be used to improve the quality of your code and reduce the number of bugs in your application.

There are many linters available for React applications, including ESLint and JSLint. These tools can be used to enforce coding standards and detect common mistakes in your code. For example, they can be configured to flag unused variables, missing semicolons, and other common errors.

You can also use code analysis tools like SonarQube to analyze your code and identify potential security vulnerabilities. These tools can be used to detect security vulnerabilities.

Applying the Principle of Least Privilege (PoLP)

The Principle of Least Privilege (PoLP) states that every user should be given the minimum amount of privileges necessary to perform their job. This helps prevent unauthorized access to sensitive information and resources.

This means that you should only give users access to the resources they need to perform their job. For example, if a user only need to view a particular pages, they should not be allowed to edit it.

In a Reactjs application, you can implement the PoLP by using role-based access control (RBAC). This allows you to define roles for different types of users and assign permissions to those roles. For example, you can define an “admin“ role and assign it permission to edit pages.

HTTP Security Headers and How to Set Them in Next.js

Security Headers are a type of HTTP header that helps us describe our site so we can help protect it against various potential attacks.

What is a Security Header

An HTTP (HyperText Transfer Protocol) header is simply something that we use to pass some extra information with HTTP requests and responses. Think of them as notes that give more context to the main document. Each header is simply a key-value pair separated by a colon.

When you visit a website, the server receives a request from the browser. This is a request for more information about the site you are visiting, which is exchanged from the HTTP request headers. The server then responds, and the response will include HTTP response headers.

An example of the HTTP Header is the “Authorization“ header. It simply authenticates the user agent (software that acts on behalf of a user) with a server.

Authorization: Bearer <Some token>

So what is an HTTP security header?

We use security headers to help describe our sites, specifically to protect against various potential attacks. When a user navigates to the site, the server will respond with response headers that provide the browser with information on how to handle things.

By adding security headers to the response of our pages, we can minimize the possibility of the following attacks happening in our application.

  • Cross-Site Scripting (XXS)

  • Clickjacking

  • Code injection

Examples of Security Headers

Let’s take a look at some of the important security headers that you may want to implement:

If you are already familiar with security headers, skip ahead and set them up in Next.js here - Adding security headers to a Next.js app.

  1. Content Security Policy

The content security policy header allows you to set a policy for which domains are executable scripts. It’s like setting a policy for children in a video streaming service. You can specify what they can and cannot watch and it’s the same idea here.

Sometimes, your site will need to execute scripts that come from origins other than your own application. With this header, we can fine-tune exactly which domains should be considered safe or ‘whitelisted‘ and which should not.

We can also control the kind of resources we load into the site in more detail. For example, we could restrict loading scripts to our own domain but allow fonts or images to be loaded from a specific outside source.

Let’s take a look at a few examples:

To allow content from any domain as if the policy was not set

Content-Security-Policy: default-src *

Or set the own origin as the only trusted domain.

Content-Security-Policy: default-src 'self'

You can also specify any domain other than your own trusted domain.

Content-Security-Policy: default-src 'https://someotherdomain.com'

We can fine-tune our content security policy to set separate policies for resources like images, fonts, styles, media, scripts, and more like this

Content-Security-Policy: default-src 'self'; font-src 'self' 'https://fonts.googleapis.com'; image-src *.somewhere.com; script-src 'self'

In the above policy, we set the following:

  • We set the default source for the content as our site’s origin, so this will be used for all resources unless otherwise specified.

  • We allow fonts to be loaded from our own site or from Google fonts

  • Images can be trusted from anywhere that ends in .somewhere.com

  • Scripts are only trusted from our own site

  1. Permissions Policy (Previously ‘Feature Policy‘)

The recently renamed Permissions Policy Security header allows you to control which browser APIs are allowed to be used within your site document or any frames located in the document.

Things like geolocation, camera, microphone, and many more can all be disabled if you know your site does not require them, which can reduce the possibility of attacks coming through these avenues.

Permissions-Policy: camera=(); battery=(self); geolocation=(); microphone=('https://somewhere.com')

In the above example, we set the policies of several browser features:

  • The camera is empty, which means we deny the use of video input devices.

  • Battery Status API is allowed within your own domain

  • Geolocation is empty, which means we deny its use

  • Audio input devices are allowed for the origin stated

  1. X-Frame-Options

The X-Frame-Options header controls whether or not your site can be loaded within a frame. Allowing this can be dangerous because you leave yourself open to clickjacking attacks.

Clickjacking is an attack that involves tricking the user into clicking something that they believe to be something else. Attackers may disguise elements, and the user is unable to see what they are actually clicking on.

We can help protect ourselves against this attack by preventing our site from being loaded within a frame.

There are two options for this header:

X-Frame-Options: deny

This recommended option is to deny your site being loaded in frames, which is shown above using the ‘deny‘ option:

X-Frame-Options: SAMEORIGIN

The “SAMEORIGIN“ option allows the site to be loaded within a frame while serving the site is the same as the one in the frame.

  1. X-Content-Type-Options

The X-Content-Type-Options header ensures that the MIME (Multipurpose Internet Mail Extensions) types specified in Content-Type are adhered to, and we avoid what is known as ‘MIME type sniffing‘. Browsers might try and determine the MINE type based on the response content instead of what is specified in the Content-Type header.

It only has one option, and it prevents the browser from trying to change the Content-Type away from what was declared.

X-Content-Type-Options: nosniff

  1. Referrer policy

With this header, we can control what information is sent when a user navigates from one origin to another. Consider a user that clicks a link on what we will call the original site. This link takes a user to a different domain. When this happens, some information is sent to the new domain. Information about where the user came from.

There are several different options we can set with this header.

Referrer-Policy: origin-when-cross-origin

The ‘origin-when-cross-origin‘ options send the path, origin, and query string with the same-origin request from equal protocol levels. An example of an equal protocol level would be from HTTPS to HTTPS:

If the request is cross-origin, only the origin is sent.

Referrer-Policy: strict-origin-when-cross-origin

The 'strict-origin-when-cross-origin' option is similar to the previous option. The path, origin, and query string are included with same-origin requests regardless of protocol level.

When making cross-origin requests, only the origin is sent as long as the protocol level is the same and omits the header otherwise.

Referrer-Policy: strict-origin

With 'strict-origin', only the origin is sent when the protocol level is equal and otherwise omits the header.

Referrer-Policy: no-referrer

The 'no-referrer' setting will omit the referrer header.

There are further options that you can check out here

  1. Strict Transport Security

With the Strict Transport Security header, you can specify that your site should only be accessed using the HTTPS Protocol. Here, we can specify the time in seconds that the browser will remember this protocol.

Strict-Transport-Security: max-age=31536000;

You can also extend the instruction to all subdomains with all optional settings:

Strict-Transport-Security: max-age=31536000; includeSubDomains

You may think that setting up a redirect from ‘http://' to 'https://' is enough, but attackers could still seize sensitive information. Setting up a Strict Transport Security setting will make this attack far more difficult.

Adding Security Headers to a Next.js App

Now that we've had a look at some security headers, let's quickly implement them in a Next.js app. Also, feel free to explore some of the other security headers available.

In Next.js, we can set security headers from a next.config.js file located at the root of your project.

// next.config.js
module.exports = {
  async headers() {
    return [

    ]
  },
};

We return an array of headers that we specify inside JavaScript objects. You can choose to apply headers that will be sent with every route request or on a route-by-route basis. Let's set some of the headers that we previously explored.

// next.config.js
module.exports = {
  async headers() {
    return [
        {
          source: '/(.*)',
          headers: [
            {
              key: 'Content-Security-Policy',
              value:
                "default-src 'self'; font-src 'self' 'https://fonts.googleapis.com'; img-src 'self' *.somewhere.com; script-src 'self'",
            },
            {
              key: 'X-Frame-Options',
              value: 'DENY',
            },
            {
              key: 'X-Content-Type-Options',
              value: 'nosniff',
            },
            {
              key: 'Referrer-Policy',
              value: 'origin-when-cross-origin',
            },
            {
              key: 'Permissions-Policy',
              value: "camera=(); battery=(self); geolocation=(); microphone=('https://somewhere.com')",
            },
          ],
        },
      ];
  },
};

In the above example, we set our security headers for all routes in our site indicated by the source /(.*). You could also set security headers on a page-by-page basis like this 👇

// next.config.js
module.exports = {
  async headers() {
    return [
        {
          source: '/profile',
          headers: [
            {
              key: 'Content-Security-Policy',
              value:
                "default-src 'self'; font-src 'self' 'https://fonts.googleapis.com'; img-src 'self' *.somewhere.com; script-src 'self'",
            }
          ]
        },
       {
          source: '/blog',
          headers: [
            {
              key: 'Content-Security-Policy',
              value:
                "default-src 'self'",
            }
          ]
        },
      ];
  },
};

Now, spin up a development server in your Next.js application and navigate to the route after setting your security headers. You should be able to see the headers you set in the network tab of the developer console.

Congratulations, your site is more secure than it was before. Customize the security header to suit your application, as each site has its own specific requirements.

Best Practices for Storing Access Tokens in the Browser

Browsers offer various solutions to persist data. When storing tokens, you should weigh the choice of storage against the security risks.

Web applications are not static sites but a careful composition of static and dynamic content. More often than not, the application logic runs in the browser. Instead of fetching all content from the server, the application runs Javascript under the browser that fetches data from the backend API and updates the web application presentation accordingly.

To protect access to data, organizations should employ OAuth 2.0 With OAuth 2.0, a Javascript application needs to add an access token to every request to the API. For usability reasons, Javascript applications don’t usually request the access token on demand but store it. The question is, how do you obtain such an access token within Javascript? And when you get one, where should the application store the token so that it can add it to the request when needed?

This section discusses different storage solutions available in browsers and highlights the security risks associated with each option. After reviewing the threat, it describes a solution in the form of a pattern that provides the best browser security options for Javascript applications that must integrate with OAuth-protected APIs.

For more details, check out this useful article.

References

https://blog.bitsrc.io/frontend-application-security-tips-practices-f9be12169e66

https://blog.bitsrc.io/top-7-frontend-security-attacks-2e2b56dc2bcc

https://griddynamics.medium.com/front-end-security-best-practices-8c47d23caf62

https://dev.to/akshay_varma/mitigating-xss-risks-best-practices-for-web-applications-2e04?context=digest

https://medium.com/@eddyos05/security-practices-that-frontend-developers-should-know-6bcc2b8ebbbd

https://dev.to/rigalpatel001/preventing-clickjacking-attacks-in-javascript-39pj?context=digest

https://www.freecodecamp.org/news/types-of-cyber-attacks-to-know/

https://levelup.gitconnected.com/cors-finally-explained-simply-ae42b52a70a3

https://blog.bitsrc.io/enhance-javascript-security-with-content-security-policies-5847e5def227

https://medium.com/better-programming/frontend-app-security-439797f57892

https://blog.kieranroberts.dev/http-security-headers-and-how-to-set-them-in-nextjs

https://www.turing.com/kb/reactjs-security-best-practices?ref=dailydev#using-https

0
Subscribe to my newsletter

Read articles from Tuan Tran Van directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tuan Tran Van
Tuan Tran Van

I am a developer creating open-source projects and writing about web development, side projects, and productivity.