Maximize Code Security in Your NestJS Applications (Part 2)

Maximize Code Security in Your NestJS Applications (Part 2)

Sunny Sun Lv4

Top Secure Code Best Practices for NestJS Developers

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

It is a common myth that security is the final step in the software development cycle, either in the form of a penetration test or through the use of a static scanning tool. However, this approach is not enough.

Instead, security should be integrated into every development process step, from design to coding and testing. Security considerations should be a key part of the planning and development a software application rather than an afterthought.

In this article, We 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.
If you are interested to learn more, welcome to my NestJS course.

  • Title: Maximize Code Security in Your NestJS Applications (Part 2)
  • Author: Sunny Sun
  • Created at : 2023-01-09 00:00:00
  • Updated at : 2024-07-09 21:32:34
  • Link: http://coffeethinkcode.com/2023/01/09/maximize-code-security-in-your-nestjs-applications-part-2/
  • License: This work is licensed under CC BY-NC-SA 4.0.