Top Ten Secure Code Best Practices for NestJS Developers

Top Ten Secure Code Best Practices for NestJS Developers

Sunny Sun Lv4

** Updated on 29-July-2024
In the ever-evolving landscape of cyber threats, recent breaches at companies like Optus and Medibank serve as stark reminders of the importance of code security. As developers, we hold the power to write code that acts as a fortress, safeguarding user data and application functionality. But how do we achieve this level of security?

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.

What is OWASP top 10

The OWASP (Open Web Application Security Project) Top 10 is a globally recognized standard outlining the most critical security risks to web applications. This annually updated report provides a benchmark for organizations to assess and address potential vulnerabilities.

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.

This post will walk through a few risks and the best practices that you can follow to prevent the risk.

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.

1
2
3
4
5
6
7
8
9
10
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.

1
2
3
4
5
6
7
8
@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.

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.

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).

1
2
3
4
5
6
7
8
9
10
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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.

1
2
3
4
5
6
7
8
9
10
11
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.

1
2
3
4
5
6
7
8
9
10
11
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.

In Part 1 of this article, We walk through 4 common security risks and their prevention in the context of NestJS. It is worth noting that although NestJS is used here (as one of my favored API frameworks), those best practices are framework agnostic.

Let’s continue to dive into other common security vulnerabilities.

Injection

As one of the most well-known vulnerabilities, Injection occurs when an attacker manages to execute arbitrary code or commands by injecting them into an app. Injection attacks can take many forms, such as SQL injection, command injection, and expression injection.

Although injection is a well-known risk, it is still occurring frequently. Some recent incidents include the 2017 Equifax data breach affecting 147 million users, the 2018 British airway data breach leaking 380,000 credit card details, and the 2019 Capital One breach exposing 100 million users’ personal information due to SQL injection.

Below is an example of SQL injection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Injectable } from '@nestjs/common';  
import { Connection } from 'typeorm';

@Injectable()
export class ClientService {
constructor(private connection: Connection) {}

async getClients(name: string) {
const query = `SELECT * FROM client WHERE name = '${name}'`;
return await this.connection.query(query);
}
}
// Client Controller
@Controller('users')
export class ClientController {
constructor(private clientService: ClientService) {}

@Get('search')
async searchClient(name: string) {
return this.clientService.getClients(name);
}
}

In this example, the ClientService class use the name parameter to construct an SQL query that is executed against a database. If the name the parameter isn’t properly sanitized; an attacker could inject malicious code into the query by including special characters. For example, an attacker could send a request with a name parameter such as '; DROP TABLE client; --, which would delete the client table.

SQL injection can be prevented by sanitizing user input and using prepared statements or parameterized queries whenever possible. Here is an improved version of the previous example.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Injectable } from '@nestjs/common';  
import { Connection } from 'typeorm';

@Injectable()
export class ClientService {
constructor(private connection: Connection) {}

async getClients(name: string) {
const query = 'SELECT * FROM client WHERE name = $1';
const params = [name];
return await this.connection.query(query, params);
}
}

The revised ClientService uses a parameterized query, which helps to ensure that user input is treated as data rather than executable code.

Another common type of Injection attack is OS command injection. It happens when an attacker injects an arbitrary OS command and executes it; the injection can be done via a request header or parameter, etc. Here is an example.

1
2
3
4
5
6
7
@Injectable()  
export class ClientService {
public executeCommand(command: string): void {
const commandArray = command.split(' ');
spawn(commandArray[0], commandArray.slice(1));
}
}

Without proper sanitizing or validating the command input, the above code opens the door for a malicious command like “rm rf /var/www”.

To prevent OS command injection, the best way is to replace the OS commands in the app with framework-specific API. Alternatively, we should escape and validate the user input to ensure that only the expected input will pass the validation.

Lack of Resources and Rate limiting

Many clients can call an API at the same time. If the amount of simultaneous requests exceeds the limit, the API will become unresponsive or even crash.

This vulnerability can be triggered by a sudden surge of legitimate requests during peak hours or malicious DDoS attacks. One of the recent DDoS attacks is the 2020 AWS web service attack .

Any API endpoint without rate limiting can be vulnerable when an attacker sends a large number of requests in a short period of time. To prevent it, we can use a rate-limiting middleware in NestJS. You have a few choices available, such as [nestjs/throttler](https://github.com/nestjs/throttler) or e``[xpress-rate-limit](https://github.com/express-rate-limit/express-rate-limit)``.

In the below example, we use nestjs/throttler to restrict that maximum of 10 requests from the same IP can be made to a single endpoint in 1 minute. It applies to all the incoming requests for the app.

1
2
3
4
5
6
7
8
9
@Module({  
imports: [
ThrottlerModule.forRoot({
ttl: 60,
limit: 10,
}),
],
})
export class AppModule {}

There are other options available to customize the throttling. For example, you can use @SkipThrottle decorator to disable rate limiting for an endpoint or use @Throttle() decorator to override the limit and ttl set in the global module.

Besides the number of requests, the following limits should also be considered.

  • Execution timeout: If a request takes too long to complete, it should be terminated.
  • Payload size/Maximum number of data in response, if a request returns a potentially large amount of data
  • Maximum allocable memory: excessive memory usage can cause the App to crash.

Identification and Authentification Failure

Identification and authentication failures are vulnerabilities related to applications’ authentication and identification processes.

It can happen when a system or application does not have robust methods in place to verify the identity of users or when the authentication process can be easily bypassed or manipulated.

This vulnerability can come in many different forms. The most common one is known as session hijacking. Here is an example of how this could happen in a NestJS application:

  • The attacker intercepts the normal user’s session cookies. These cookies may contain information such as the user’s ID or Role and other identifying information.
    1
    2
    3
    4
    // Cookies content  
    eyJ1c2VySWQiOjEyMzQ1LCJ1c2VyUm9sZSI6Im5vcm1hbCJ9
    // JSON
    {"userId":12345,"userRole":"normal"}
  • The attacker modifies the session cookies to change the user Role and other identifying information to match an administrative account.
  • The attacker returns the modified cookies to the server, pretending to be the administrator. The server receives the modified cookies and grants the attacker access to the app with administrative privileges.

The above example is obviously caused by insufficient authentication at the server. One way to prevent this type of attack in NestJS is to implement the authentication using JWT (JSON Web token). NestJS provides a [@nestjs/jwt](https://github.com/nestjs/jwt) package for JWT manipulation. You can find more details on implementing JWT in your NestJS app here .

You can consider implementing MFA (multi-factor authentication) to tighten the authentication further. The most popular form of MFA is OTP (one-time passcode).

You can create your own OTP service to store the one-time passcode and manage the sending via phone or email. If you don’t want to reinvent the wheel, we can use existing libraries like otplib .

Below are the basic steps to use otplib in NestJS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// install Otplib  
npm install otplib
// import it
import * as OTPLib from 'otplib';

// generate a secrete
const secret = OTPLib.authenticator.generateSecret();
// then, we can gerenate a QRCode url to show a QRCode in your app
// we should save the generated secret in database for later use
const otpUrl = OTPLib.authenticator.keyuri('user', 'The App name', secret);

// Now user scan the QRCode and sent it to the NestJS endpoint
// we can use the built in authenticator.verify to validate the otp token
import { authenticator } from 'otplib';

authenticator.verify({
token: token // Sent from Client,
secret: secrete // previously saved secrete
})

If MFA can’t be implemented, the other options to consider for additional security includes the following:

  • security questions
  • CAPTCHA
  • require strong password

Missing Object level access control

Object-level access control is a security mechanism that can control access to a specific object or resource based on the permissions or roles of the user requesting access.

Below is an example of how the risk might occur in a NestJS application:

1
2
3
4
5
6
7
8
9
10
11
12
import { Injectable } from '@nestjs/common';  
import { ClientService } from './client.service';

@Injectable()
export class AttachmentController {
constructor(private clientService: ClientService) {}

@Get('/document/:id')
public async getFile(id: string): Promise<any> {
return await this.clientService.getAttachmentById(id);
}
}

In this example, thegetFile method in the AttachmentController doesn’t have object-level access control in place to ensure that only the attachment owner or users with permissions can access it. An attacker could potentially access any file in the system by guessing attachment Ids.

To prevent the risk, we can verify the user is the owner or has permission to access the object or resource.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Injectable()  
export class AttachmentController {
constructor(private clientService: ClientService) {}

@Get('/document/:id')
public async getFile(id: string): Promise<any> {
const currentUser = getCurrentUser();
const document = await this.clientService.getAttachmentById(id);
if (currentUser.id !== document.ownerId) {
throw new ForbiddenException();
}
return document;
}
}

Please note that the above is a contrived example based on the assumption that only the attachment file’s owner can access it.

To further reduce the risk, we can also use random values that are difficult to predict as record Ids. This can help to prevent attackers from guessing or enumerating record IDs. Below is an example to use uuid module to generate a random unique value for the document id.

1
2
3
4
import { v4 as uuid } from 'uuid';  
const document = {
id: uuid(),
ownerId: currentUser.id,

The best practice is to ensure the server-side functions are protected with a role-based authorization guard in your NestJS app.

Final thoughts

Traditionally, security has often been treated as an afterthought in the software development lifecycle, with penetration testing or static code analysis serving as a final line of defense. This reactive approach is no longer sufficient in today’s threat landscape.

To build resilient applications, security must be embedded into every phase of development. From initial design to deployment, a proactive security mindset is essential. By incorporating security principles early on, organizations can significantly reduce vulnerabilities and protect sensitive data.

This article discuss several common security risks that can impact NestJS applications, there are many others that need to be considered. I hope you found this article helpful in building a secure and resilient app.

  • Title: Top Ten Secure Code Best Practices for NestJS Developers
  • Author: Sunny Sun
  • Created at : 2023-01-09 00:00:00
  • Updated at : 2024-07-29 20:55:25
  • Link: http://coffeethinkcode.com/2023/01/09/top-10-secure-code-best-practices/
  • License: This work is licensed under CC BY-NC-SA 4.0.