browse all posts
Image of Trevor-Indrek Lasn

Trevor-Indrek Lasn

Frontend Security Checklist: Proactive Measures and Best Practices for All Frameworks

Frontend
Security

Dec 23, 2023

Overview of Frontend Application Security

Ensuring the security of frontend applications is a crucial task. This guide delves into common vulnerabilities that are encountered across various frontend frameworks such as React, Angular, and Vue.js.

I aim to provide not only an understanding of these vulnerabilities but also practical solutions and code examples to effectively mitigate them.

Disclaimer: The technologies, companies, and products mentioned in this article are referenced based on my personal and professional experience.

My mention of these entities, including those utilized on Lebohire or in past projects, does not imply any association, endorsement, or partnership with them. They are cited purely for informational and illustrative purposes, reflecting my own usage and familiarity in various contexts.

1. Cross-Site Scripting (XSS)

Understanding and Preventing XSS Attacks

  • Injection of Malicious Scripts: Attackers exploit user inputs that aren't properly sanitized, inserting scripts into web pages. For instance, a script entered in a blog comment field could be stored and rendered in every visitor's browser.
  • Execution in User Context: The script executes within the browser of any user visiting the compromised page, acting under the user's session.
  • Stealing Information: Such scripts can access sensitive data like cookies or session tokens, leading to potential session hijacking.
  • Manipulating DOM: The attack can alter the webpage's Document Object Model (DOM), leading to website defacement, phishing, or redirects to malicious sites.

Types of XSS Attacks

  • Stored XSS: The malicious script is stored on the server and delivered to all users viewing the infected page.
  • Reflected XSS: The script is reflected from a web server, typically via a malicious link.
  • DOM-based XSS: The payload executes by modifying the DOM in the client-side environment, like a web browser, without altering the server response.

Framework Vulnerabilities

  • Even frameworks like React, Vue, and Angular, which take measures against XSS (e.g., escaping HTML), can be vulnerable due to improper feature use, third-party libraries, or non-standard implementations.

Sanitizing User Input in React

import React, { useState } from 'react';
import DOMPurify from 'dompurify';

const UserComments = () => {
  const [comments, setComments] = useState([
    // Assume these comments are fetched from a server
    { id: 1, content: '<script>alert("XSS Attack!")</script>Great article!' },
    { id: 2, content: 'Really enjoyed this post.' },
    // More comments...
  ]);

  const createMarkup = (htmlContent) => {
    return { __html: DOMPurify.sanitize(htmlContent) };
  };

  return (
    <div>
      <h2>User Comments</h2>
      <ul>
        {comments.map((comment) => (
          <li key={comment.id} dangerouslySetInnerHTML={createMarkup(comment.content)} />
        ))}
      </ul>
    </div>
  );
};

export default UserComments;

Explanation

  • State Initialization: useState is used to simulate a list of user comments. In a real-world scenario, this data would likely come from an external source like a database.
  • Sanitizing Function: createMarkup is a helper function that takes raw HTML content and returns an object with a sanitized version of that HTML. DOMPurify.sanitize is used to clean the HTML content, removing any potentially malicious scripts.
  • Rendering Comments: Comments are rendered using the dangerouslySetInnerHTML property, which is React's way of inserting raw HTML into the DOM. However, since we sanitize the content first, the risk of XSS is mitigated.

Keep in mind, you shouldn't have to use dangerouslySetInnerHTML in most cases, as the name suggest. Most modern frontend frameworks are safe by default. As long as you don't use dangerouslySetInnerHtml.

Points to Note

  • Using dangerouslySetInnerHTML: React advises caution when using dangerouslySetInnerHTML because of the risk of XSS attacks. However, when combined with a sanitizer like DOMPurify, it becomes a safer option. (dangerouslySetInnerHtml NOT RECOMMEND TO USE IN PRODUCTION)
  • Sanitization Library: DOMPurify is a widely used and trusted library for sanitizing HTML content against XSS attacks. It removes dangerous parts of the HTML and keeps the safe ones.
  • Real-World Implementation: In a real application, you would fetch user-generated content from a server or database. It's crucial to sanitize this content before rendering it to the browser.

2. Content Security Policy (CSP) in Web Server Configuration

Implementing a Content Security Policy (CSP) is a crucial step in securing your web application against various types of attacks, including Cross-Site Scripting (XSS).

A CSP allows you to specify which resources can be loaded and executed by the browser, significantly reducing the risk of malicious content execution.

I utilize Caddy as the web server for hosting the compiled files and JavaScript for Lebohire's website. Caddy is a web server written in the Go programming language. In a nutshell, it's an alternative to nginx.

Caddy Example

I defined a CSP header that restricts JavaScript execution to certain trusted domains.

# CSP Header
header Content-Security-Policy "script-src 'self' 'unsafe-inline' http://localhost:5173 https://static.cloudflareinsights.com https://www.clarity.ms https://accounts.google.com https://upload-widget.cloudinary.com https://maps.googleapis.com https://www.googletagmanager.com;"

Here's an explanation of the current policy:

  • 'self': Allows scripts hosted on the same origin as the web page.
  • 'unsafe-inline': Allows the use of inline scripts, though it's generally recommended to avoid this for better security.

Specified URLs: Only scripts from these whitelisted sources are allowed to run.

Nginx Example

In Nginx, you can set the CSP header similarly within the server block of your configuration file.

Here's an example that mimics the policy from my Caddyfile:

codeserver {
    # ... other configuration ...

    add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' http://localhost:5173 https://static.cloudflareinsights.com https://www.clarity.ms https://accounts.google.com https://upload-widget.cloudinary.com https://maps.googleapis.com https://www.googletagmanager.com;"

    # ... other configuration ...
}

Best Practices and Considerations

Avoid 'unsafe-inline' if Possible: While it's included in my policy, it's generally safer to avoid inline scripts. Instead, move inline scripts to external files hosted on your server or a trusted domain.

Dynamic Hashes for Inline Scripts: If inline scripts are necessary, consider using hashes or nonces to allow specific scripts.

Regularly Review and Update Your CSP: As your application evolves, regularly review your CSP to ensure it aligns with your current resources and third-party integrations.

Testing: Before deploying changes to your CSP in a production environment, thoroughly test them to ensure they don't inadvertently break any functionality.

Report-Only Mode: Initially, you might want to use the CSP in report-only mode (Content-Security-Policy-Report-Only) to observe its impact without enforcing it. This helps in identifying and fixing any issues before enforcing the policy.

By carefully configuring and regularly updating your CSP, you can significantly enhance the security of your web application.

3. Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery (CSRF) is a type of security attack on web applications. In a CSRF attack, the attacker tricks a legitimate user into submitting a request that they did not intend to.

This is typically done by embedding malicious requests into a website or email the user interacts with.

The attack exploits the fact that the user's browser is already authenticated with a site (e.g., logged into a bank or email account), and the site can't distinguish the illegitimate request from a legitimate one.

Here's a brief example:

User Authentication: A user logs into a banking website and their browser stores authentication cookies.

Malicious Request: The user then visits a malicious site, which contains a hidden form that triggers a fund transfer on the banking site.

Execution of Request: When the user clicks something on the malicious site, the hidden form is submitted to the banking site. Because the user's browser is still authenticated with the bank, the bank processes the request as if it were legitimate.

To protect against CSRF attacks, use anti-CSRF tokens in your forms.

These tokens are unique to each user session and are validated on the server-side. This ensures that the request is coming from the site's own form and not from an external, malicious source.

Mitigating CSRF Attacks

  • Anti-CSRF Tokens: Use anti-CSRF tokens in forms and validate them on the server side. This method is applicable across all frameworks.

Frontend implementation (Svelte)

This Svelte code defines a web form that helps prevent Cross-Site Request Forgery (CSRF) attacks.

<script>
  import { onMount } from 'svelte';
  import axios from 'axios';

  let csrfToken = '';

  onMount(async () => {
    try {
      const response = await axios.get('/api/get-csrf-token');
      csrfToken = response.data.csrfToken;
    } catch (error) {
      console.error('Error fetching CSRF token:', error);
    }
  });

  async function submitForm() {
    const formData = new FormData();
    formData.append('_csrf', csrfToken);
    // Append other form data here

    try {
      const response = await axios.post('/api/submit-form', formData);
      // Handle success
    } catch (error) {
      // Handle error
    }
  }
</script>

<form on:submit|preventDefault={submitForm}>
  <input type="hidden" name="_csrf" bind:value={csrfToken} />
  <!-- Your form fields go here -->
  <button type="submit">Submit</button>
</form>

Backend implementation (Express.js)

On the server side, you can use a package like helmet to create and validate CSRF tokens.

const express = require('express');
const helmet = require('helmet');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');

const app = express();

app.use(helmet());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());

const generateCsrfToken = () => {
  return crypto.randomBytes(100).toString('base64'); // Random token
};

// Middleware to set CSRF token
const csrfMiddleware = (req, res, next) => {
  if (!req.cookies.csrfToken) {
    const csrfToken = generateCsrfToken();
    res.cookie('csrfToken', csrfToken, { httpOnly: true });
    req.csrfToken = csrfToken;
  } else {
    req.csrfToken = req.cookies.csrfToken;
  }
  next();
};

// Endpoint to get CSRF token
app.get('/api/get-csrf-token', csrfMiddleware, (req, res) => {
  res.json({ csrfToken: req.csrfToken });
});

// Endpoint to submit form with CSRF token validation
app.post('/api/submit-form', csrfMiddleware, (req, res) => {
  if (req.body._csrf !== req.csrfToken) {
    return res.status(403).send('CSRF token mismatch');
  }
  // Handle your form submission
  res.json({ message: 'Form submitted successfully' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

In this example, helmet is used to set various HTTP headers for security.

A custom csrfMiddleware is created to generate and validate CSRF tokens. The token is stored in a cookie and sent back to the client. When the client submits a form, the server checks if the CSRF token in the form matches the one stored in the cookie. If they don't match, it returns a 403 error, indicating a CSRF attempt.

Remember, while this approach is custom and flexible, it's important to carefully implement and test CSRF protection to ensure its effectiveness.

4. Secure Data Storage and Transmission

Best Practices for Data Handling

  • Enforce HTTPS: Ensure data is encrypted during transit.
  • Prudent Data Storage: Avoid storing sensitive information in local storage or client-side cookies.

5. Managing Dependency Vulnerabilities

Regular Audits and Updates

  • Dependency Audits: Use tools like npm audit or Yarn for detecting and updating vulnerable dependencies.

6. Insecure Direct Object References (IDOR)

Enhancing Access Control

Insecure Direct Object References (IDOR) refer to a security weakness where an application provides direct access to objects based on user-supplied input.

This issue occurs when an application exposes internal implementation objects, like files, database records, or key-indexes, to users without proper authorization checks.

An IDOR vulnerability allows attackers to bypass authorization and access resources in the system directly, for example, by modifying the value of a parameter used to directly point to an object (such as database record IDs in URLs).

Here's a simple example to illustrate IDOR and how to mitigate it:

Example Scenario: Consider an application where users can view their profiles by visiting a URL like example.com/user/123, where 123 is the user's ID in the database.

Vulnerability: If there are no proper access controls, a user could change the URL to example.com/user/124 and access another user's profile.

Mitigation Steps:

Server-Side Validation:

  • Always validate that the user requesting the resource is authorized to access it.
  • This can be done by checking if the user ID of the session matches the user ID of the resource requested.
const express = require('express');
const app = express();
const session = require('express-session');

// Assuming we have a function to get user data
const getUserById = require('./getUserById');

app.use(session({
  secret: 'your-secret',
  resave: false,
  saveUninitialized: true
}));

app.get('/user/:id', (req, res) => {
  const userId = req.params.id;
  const loggedInUserId = req.session.userId;

  // Check if logged-in user ID matches the requested user ID
  if (userId === loggedInUserId) {
    getUserById(userId, (err, user) => {
      if (err) {
        return res.status(500).send('Internal Server Error');
      }
      if (!user) {
        return res.status(404).send('User not found');
      }
      res.json(user);
    });
  } else {
    res.status(403).send('Access Denied');
  }
});

app.listen(3000, () => console.log('Server is running on port 3000'));

7. Block malicious traffic

For Lebohire.com, I've implemented a security measure where visitors from certain regions, identified based on their geographic location, are required to complete an interactive captcha before accessing the site. This practice helps to maintain the integrity and security of the site's content.

Blog post image

Many cloud service providers offer the functionality to set up such geo-based access controls.

8. Cloudflare Scrape Shield

Scrape Shield is a collection of settings meant to protect your site’s content. Considering the risks posed by web scrapers, why wouldn't you take steps to protect your site's valuable content?

Blog post image

Email Address Obfuscation

By enabling Cloudflare Email Address Obfuscation, email addresses on your web page will be hidden from bots, while keeping them visible to humans. In fact, there are no visible changes to your website for visitors.

​​Background

Email harvesters and other bots roam the Internet looking for email addresses to add to lists that target recipients for spam. This trend results in an increasing amount of unwanted email.

Web administrators have come up with clever ways to protect against this by writing out email addresses, such as help [at] cloudflare [dot] com or by using embedded images of the email address. However, you lose the convenience of clicking on the email address to automatically send an email. By enabling Cloudflare Email Address Obfuscation, email addresses on your web page will be obfuscated (hidden) from bots, while keeping them visible to humans. In fact, there are no visible changes to your website for visitors.

​​Hotlink Protection

Hotlink Protection prevents your images from being used by other sites, which can reduce the bandwidth consumed by your origin server.

Server-side Excludes (SSE)

If there is sensitive content on your website that you want visible to real visitors, but that you want to hide from suspicious visitors, wrap the content with Cloudflare Server-side Excludes (SSE) tags. Browse through the documentation if you want more information.

9. DDOS Protection

A distributed denial-of-service (DDoS) attack is where a large number of computers or devices, usually controlled by a single attacker, attempt to access a website or online service all at once.

This flood of traffic can overwhelm the website’s origin servers, causing the site to slow down or even crash completely.

I personally let Cloudflare manage all my DDOS protection, so I can focus on the business code. If your domain is under their DNS, you're good to go by default.

Blog post image

Image credit: cloudflare

How to tell if you are under DDoS attack

Common signs that you are under DDoS attack include:

  • Your site is offline or overwhelmed and slow to respond to requests.
  • There are dubious requests in your origin web server logs that do not match normal visitor behaviour.

10. Environment Variables

One of the key aspects of frontend application security is protecting sensitive information like API keys, database credentials, and other confidential data.

In simple terms: Use env variables so they don't get leaked into the git repository. Especially when the repository is open source and public.

Here's how you can use environment variables with Webpack or Vite.

// Create a .env file at your project root:

// add your variables:

VITE_API_KEY=your_api_key_here

// Accessing Environment Variables:
// In your Vite project, you can access the environment variable directly:

const apiKey = import.meta.env.VITE_API_KEY;

// webpack:

REACT_APP_API_KEY=your_api_key_here

// accessing variables: 
const apiKey = process.env.REACT_APP_API_KEY;

Conclusion

Ensuring robust security in frontend applications is a dynamic, ongoing endeavor. This guide has highlighted key vulnerabilities common to frameworks like React, Angular, and Vue.js, and provided strategic insights to bolster defense mechanisms.

By embracing regular updates, ensuring secure data transmission, diligently sanitizing inputs, and adhering to best coding practices, developers can significantly enhance their application's security posture. These proactive steps are indispensable across all frontend frameworks, forming the backbone of a resilient defense against the constantly shifting landscape of web threats.

Disclaimer and Advisory Note:

The content presented in this article is intended solely for educational purposes. It should not be construed as legal advice or a comprehensive security solution. Efforts have been made to ensure the accuracy of the information and strategies discussed, but the author and publisher assume no responsibility for any inaccuracies, omissions, or misinterpretations. This article should not be the sole source of information for web security.

Readers are encouraged to consult professional legal advisors or security experts before implementing any of the discussed security measures. The application of this information is at the reader's own risk. The author and publisher disclaim any liability for actions taken based on this information.

Find your dream software engineering job with us

Browse and find your perfect job. Build your profile, showcase your work, get hired by a top company.

Get Hired By A Top Company

This article was published on Lebohire.com: A specialized hiring platform dedicated to software engineering jobs.