Maximize Code Security in Your NestJS Applications (Part 1)

Maximize Code Security in Your NestJS Applications (Part 1)

Sunny Sun lol

Top Secure Code Best Practices for NestJS Developers

As a developer, we all know how critical is code security. The recent data breaches at Optus and Medibank highlight the importance of code security again. So, the question is: how can we write secure code to prevent various types of attacks in web applications? It is essential to follow best practices to write secure code, so our App is protected against vulnerabilities and threats.

Before we dive into how to prevent the security risk? let’s first examine the most common types of security risks. This will give us a better understanding of the challenges to keep our App secure.

The OWASP Top 10 is a widely accepted list of the most critical security risks for web applications, as determined through consensus among industry experts. Below is the list of the top 10 risks in 2017 and 2021.

Source: [https://owasp.org/www-project-top-ten/](https://owasp.org/www-project-top-ten/)Source: https://owasp.org/www-project-top-ten/

Many of the top 10 are critical for the security of web Apps.

As part 1 of a two-part article, I will walk through a few risks and the best practices that you can follow to prevent the risk.

They include:

Broken access control

Broken function access is one of the most common risks. It occurs when an attacker is able to access unauthorized functions or resources. One real-world example is the Snapchat incident on Jan 2014 .

To prevent this risk, it is important to follow the principle of least privilege. This means that access should always be denied by default, and privileges should only be granted on an as-needed basis.

We can use access control mechanisms such as role-based access control (RBAC) or access control lists (ACLs) to restrict access to functions or resources based on a user’s role or permissions.

Here’s an example of RBAC using Guards in a NestJS application:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AdminRoleGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return user.role === 'admin';
  }
}

@Controller('cats')
export class CatsController {
  @UseGuards(AdminRoleGuard)
  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

In the above code snippet, we create an AdminRoleGuard that implements the CanActivate interface provided by NestJS. It checks the role of the current user and returns true when the user is an admin. We then use the @UseGuards decorator to apply the AdminRoleGuard to the findAll method, which will restrict access to the endpoint for users withadmin role only.

The access control mechanisms should be applied using centralized functions from a proven framework, to ensure it is safe and easy to maintain.

It is also recommended to have unit tests that tests the necessary Guards being applied on a controller. Thus if the Guard is accidentally removed, the unit test will catch it.

Server-side request forgery (SSRF)

SSRF is a type of cyber attack in which an attacker induces a server to make unintended requests on their behalf. These requests can be used to access restricted resources from internal networks.

To prevent SSRF, It is essential to properly validate user input. Below is an example of an endpoint vulnerable to SSRF risk.

import { Controller, Get, Res, HttpStatus, Query } from '@nestjs/common';

@Controller()
export class CatsController {
  @Get()
  async getData(@Query('url') url: string, @Res() res) {
    const response = await fetch(url);
    return await response.json();
  }
}

In the above example, the App makes a request to the URL sourced from url query parameter and returns the response data to the client. Obviously, it is vulnerable to SSRF attacks because an attacker can send a request to the server with a malicious URL that accesses restricted resources from the internal network.

We should validate the URL parameter to prevent the risk as below.

import { Controller, Get, Res, HttpStatus, Query } from '@nestjs/common';
import { isURL } from 'validator';

@Controller()
export class CatsController {
  @Get()
  async getData(@Query('url') url: string, @Res() res) {
    if (!isURL(url)) {
      return res.status(HttpStatus.BAD_REQUEST).send('Invalid URL');
    }

    const response = await fetch(url);
    return await response.json();
  }
}

To further improve security, we shouldn’t allow users to pass in URLs directly in query parameters. Instead, we should use an existing service to retrieve the data from a trusted API.

@Controller()
export class CatsController {
  @Get()
  async getData(@Query('name') dataName: string, @Res() res) {
    const response = await dataService.GetDataByName(dataName);
    return await response.json();
  }
}

There are other ways to prevent SSRF attacks:

  • Only make requests to trusted sources (i.e. known APIs or services)

  • Implement security headers (i.e. headers like “X-Frame-Options ”) to prevent clickjacking attacks and other types of malicious requests.

  • Use a Content Security Policy(CSP) to specify which sources are allowed to make requests on behalf of your application.

In NestJS, you can use helmet to easily set up security headers and a Content Security Policy.

Mass Assignment

Mass assignment is a vulnerability in that an attacker is able to modify multiple object properties by sending a malicious request to your App.

In the below example, a new user is created based on the data coming from the request body. It is vulnerable to mass assignment attacks because an attacker can send a request with malicious data that overwrites sensitive fields in the Client object (i.e. role or password).

import { Controller, Post, Body } from '@nestjs/common';

@Controller("client")
export class ClientController {
  @Post()
  create(@Body() body) {
    const client = new Client(body);
    return await client.save();
  }
}

To prevent mass assignment, we can define a whitelist of allowed properties for each object. In the below example, we implemented a white list of properties to prevent overwriting of sensitive fields.

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Client{
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  role: string;

  @Column()
  password: string;

  @Column(})
  email: string;
}

@Controller('client')
export class ClientController {
  constructor(private clientService: ClientService) {}

  @Post()
  async create(@Body() client: Pick<User, 'name' | 'email'>) {
    return await this.clientService.create(client);
  }
}

Here, we use the TypeScript Pick type to define a whitelist of properties for the User entity. The @Body decorator is then used to bind the request body to the user parameter, which will only include the allowed properties. This prevents an attacker from modifying other properties of the User entity through mass assignment.

Other ways to prevent mass assignment include:

  • Use a reduced DTO, instead of a general DTO. For example, create a InsertClientEntity and UpdateClientEntity. These DTOs only contain properties that are allowed in the insert and update operation.

  • Avoid directly binding to an object coming from the client side.

Sensitive information exposure

Sensitive information includes things like passwords, API keys, and other confidential data. Any data that contains personal information or payment-related information are sensitive.

Often, when designing web API, excessive data are returned to the client.

import { Controller, Get, Param } from '@nestjs/common';
import { Client} from './client/client.entity';

@Controller()
export class ClientController {
  @Get('clients/:id')
  async getClient(@Param('id') id: string): Promise<Client> {
    // Return all fields for the client
    return await Client.findById(id);
  }
}

In this example, the getClient method is returning all fields for the client including sensitive data like role or password. Although these data aren’t consumed or displayed by clients, they still can be intercepted and exposed by attackers.

To prevent sensitive personal data exposure, we should only return the necessary data of the client, which is name and email fields in this case. In a nutshell, we should only expose the minimum amount of data.

import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { Client} from './client/client.entity';

@Controller()
export class ClientController {
  @Get('clients/:id')
  async getClient(@Param('id') id: string): Promise<Client> {
    // Only return the name and email
    return await Client.findById(id).map(c => {c.name, c.email});
  }
}

To prevent sensitive data exposure, below are other guidelines to follow:

  • Do NOT store sensitive information to version control. This information includes environment variables or configuration files

  • Identify sensitive information (GDPR , PCI, and PII data ) in your system, and secure them through encryption.

  • Make sure that your App uses HTTPS between the client and the server. This will prevent sensitive data from being intercepted during transmission.

Summary

In this article, we walk through 4 common risks and the best practices to prevent those risks within the context of NestJS.

By following these best practices, you can write secure code to ensure that your NestJS app is as secure as possible.

In part 2 of the article, we continue the discussion on other top OWASP risks.
Maximize Code Security in Your NestJS Applications (Part 2)
Secure Code Best Practices with Real-World Exampleslevelup.gitconnected.com

Happy Programming!