Skip to main content

5 Node.js security code snippets every backend developer should know

著者:
feature-nodejs-security-snippets

2024年2月28日

0 分で読めます

As backend developers, we are tasked with the crucial role of ensuring the security of our applications. Node.js is not exempt from this responsibility and its growing popularity makes it a lucrative target for hackers, making it imperative to follow best security practices when working with Node.js.

In this blog post, we will be exploring some essential Node.js security code snippets every backend developer should know in 2024. Node.js software vulnerabilities can be a significant threat to any application. They can lead to unauthorized access, data leaks, and in worst-case scenarios, a complete compromise of the system. For this reason, adhering to security best practices is not just a recommendation, but a requirement.

In the context of Node.js, this means ensuring that your code doesn't expose any loopholes that could be exploited. It means sanitizing user input to prevent injection attacks, properly treating passwords as sensitive data, and managing dependencies to prevent third-party vulnerabilities, among other practices.

We will review the following Node.js security concepts and their associated code snippets based on their effectiveness in preventing common security vulnerabilities and being very accessible to developers without requiring extra security expertise:

  1. Use the Node.js Permissions Model to restrict access to resources during runtime

  2. Implement input validation with a Fastify JSON schema

  3. Secure password hashing with Bcrypt

  4. Prevent SQL injection attacks with Knex.js

  5. Implement rate limiting with fastify-rate-limit

1. Use the Node.js Permissions Model to restrict access to resources during runtime

The Node.js Permissions Model can play a critical role in securing your applications. It is a key part of the core Node.js security model, ensuring that your applications are safe from attacks and malicious activities. Similar to the security promise that Deno brought about process-centric resource constraints, Node.js is now up to par (to an extent) with a similar permissions model.

Consider a scenario where a Node.js application needs to convert PDF files into PNG images. We could use the pdf-image npm package to accomplish this task. The pdf-image package uses child processes to carry out the conversion. To enable this, we need to use the --allow-child-process flag in the Node.js runtime (provided by the permissions model).

Below is a code snippet demonstrating this:

1const { PDFImage } = require('pdf-image');
2const path = require('path');
3
4const pdfPath = path.resolve(__dirname, 'sample.pdf');
5const pdfImage = new PDFImage(pdfPath, {
6  convertOptions: {
7    '-density': '300',
8    '-quality': '80'
9  },
10  combinedImage: true
11});
12pdfImage.convertFile().then(() => {
13  console.log('PDF converted to PNG successfully');
14}).catch((err) => {
15  console.error(`Failed to convert PDF to PNG: ${err}`);
16});

In this snippet, we're creating a new instance of PDFImage with the path to our PDF file and some conversion options. We're then calling convertFile() to convert the PDF into a PNG image.

If you're enabling the experimental permissions model in Node.js, then you'd have to explicitly run the Node.js runtime with the aforementioned --allow-child-process command-line flag, because behind the hood, the pdf-image library spawns a child process to convert these PDFs.

While the Node.js Permissions Model provides a great way to restrict access to system resources, it's also crucial to be aware of potential security vulnerabilities in the packages we use. That's where the Snyk extension for Visual Studio Code comes in. The Snyk extension can detect insecure code and vulnerable dependencies in a Node.js application. For example, the pdf-image package mentioned earlier is known to be vulnerable to command injection due to its use of child processes. This vulnerability has been known since 2018 and, unfortunately, there is no fix available as of yet.

This highlights the importance of using tools like Snyk to stay informed about security vulnerabilities in the packages we depend on. It's also a reminder that we must be mindful of how we use certain features, such as child processes, which can pose security risks if not handled properly.

As Node.js developers, we have a responsibility to ensure that the code we write is not only functional but also secure. One crucial way to enhance the security of a Node.js application is by using the built-in permissions model to restrict access to system resources during runtime. If you know that your Node.js application does not require any child process capabilities, do not enable this resource to avoid creating a bigger attack surface.

FAQ on the Node.js Permissions Model

What security threats does the Node.js Permissions Model prevent?

The Node.js Permissions Model is designed to prevent a range of security threats, including unauthorized file access, command injection attacks, and privilege escalation.

Can I customize the Node.js Permissions Model?

Yes, some of the resources governed by the Node.js Permissions Model are highly customizable. For example, when limiting access to file resources, you can specify various files or file paths such as --allow-fs-read=*.

Is the Node.js Permissions Model enough to secure my applications?

While the Node.js Permissions Model is a crucial part of application security, it's not enough on its own. You also need to adopt secure coding practices, regularly audit your code for vulnerabilities as well as your third-party dependencies for vulnerabilities, and use tools like Snyk to identify and fix potential security issues.

2. Implement input validation with a Fastify JSON schema

Input validation is a crucial aspect of backend development. If you've built APIs before with Express, Fastify, or other frameworks, you've probably realized the importance already. It is a security practice that ensures that only properly formatted data enters your system, thereby preventing potential security risks. One such way to implement input validation in your Node.js applications is by using the Fastify schema for your Fastify web applications.

Fastify schema-based approach

Fastify uses a schema-based approach for input validation. While it's not mandatory, it's recommended to leverage the JSON schema to validate your routes and serialize your outputs. JSON schema provides a contract for your data, detailing the expected data format, data types, mandatory fields, and other constraints. You can think of the Fastify route's JSON schema as using TypeScript to ensure strong typing in your code during the build process.

Consider a simple Fastify application where we are creating a new user. We can use a Fastify schema to validate the input data.

1const fastify = require('fastify')({ logger: true });
2
3const UserSchema = {
4  body: {
5    type: 'object',
6    properties: {
7      name: { type: 'string' },
8      email: { type: 'string', format: 'email' },
9      password: { type: 'string', minLength: 8 }
10    },
11    required: ['name', 'email', 'password']
12  }
13};
14
15// User creation route
16fastify.post('/users', { schema: UserSchema }, async (request, reply) => {
17  // Example user creation logic
18  // In a real application, you would replace this with actual database logic
19  const user = request.body;
20
21  // Simulating user creation
22  console.log("Creating user:", user);
23
24  // Responding with the created user (in real applications, never send the password back)
25  return reply
26    .code(201)
27    .send({ success: true, message: "User created", user: { name: user.name, email: user.email } });
28});
29
30// Server startup
31const start = async () => {
32  try {
33    await fastify.listen({ port: 6000, host: 'localhost' });
34    console.log(`Server running at http://localhost:3000/`);
35  } catch (err) {
36    fastify.log.error(err);
37    process.exit(1);
38  }
39};
40
41start();

In the above code, we define a UserSchema that requires a name, email, and password. The email must be in a valid email format, and the password must be at least 8 characters long.

Neglecting input validation can expose your application to various security risks, including server-side request forgery (SSRF) or HTTP parameter pollution attacks. SSRF attacks can trick the server into making unauthorized requests, potentially leading to data exposure, while HTTP parameter pollution can manipulate or corrupt requests, leading to unexpected behavior.

Tools like Snyk can help uncover these vulnerabilities by scanning your code for security flaws. They can also provide remediation advice to help you address these vulnerabilities, enhancing your application's overall security.

FAQ on input validation with Fastify

Is input validation necessary?

Yes, input validation is a fundamental security practice that prevents improperly formatted data from entering your system.

Can I use other validation libraries with Fastify?

Yes, Fastify is flexible and allows you to use other validation libraries such as Joi, yup, or ajv.

Does Fastify schema validation impact performance?

Fastify is designed for high performance and its schema validation has a minimal impact on performance.

4. What is a Fastify JSON schema?

Fastify schemas are used to validate the request and response data in your routes, ensuring that only data that meets specified criteria is processed. This reduces the risk of processing invalid data that could lead to security vulnerabilities or application errors.

3. Secure password hashing with Bcrypt

Storing user passwords securely is crucial in software development. In the event of a data breach, you want to ensure that the user's password information is not easily deciphered. This is where password hashing comes in. Hashing is the process of converting a given key into another value. A hash function is used to generate the new value, which should ideally provide a unique output (or hash code) for each unique input value.

If you decide to implement authentication by yourself instead of relying on a service or another library, it's vital to understand that the way you handle password security can directly affect your application's overall security.

Introduction to Bcrypt for password hashing in Node.js

Bcrypt is an algorithm and an npm package that provides a password-hashing function that is considered to be very secure. It incorporates a salt (random data) to protect against rainbow table attacks and provides a work factor configuration, which allows you to determine how CPU-intensive the hashing will be — a useful feature to prevent brute-force attacks.

Let's explore a simple use case:

1const bcrypt = require('bcrypt');
2const saltRounds = 10;
3const myPlaintextPassword = 'my_password';
4
5// Define an async function
6async function hashPassword(plaintextPassword) {
7  try {
8    const hash = await bcrypt.hash(plaintextPassword, saltRounds);
9    // Store hash in your password DB.
10    console.log(hash); // Example of how to use the hash
11  } catch (err) {
12    // Handle error
13    console.error(err);
14  }
15}
16
17// Call the async function
18hashPassword(myPlaintextPassword);

The saltRounds parameter determines the complexity of the hashing process. The higher the value, the longer the process takes, which can help safeguard against brute-force attacks. The hash function automatically salts and hashes the plaintext password. The resulting hash can then be stored in your database.

FAQ on password hashing and auth management in Node.js

What is a good salt length to use with Bcrypt?

Bcrypt automatically generates a 16-byte salt.

How often should I update my hashing algorithm or strategy?

If there's a significant advance in hashing technology or a vulnerability discovered in your current algorithm, it's advisable to update your hashing strategy. Node.js has built-in support for the scrypt algorithm as part of the core node:crypto module.

What is the work factor in Bcrypt?

The work factor determines how CPU-intensive the hashing process will be. The higher the work factor, the more secure the hash, but it will also take longer to compute. For Node.js, this can also directly impact the event-loop and responsiveness of your Node.js application, depending on how you generate the hash.

4. Prevent SQL injection attacks with Knex.js

SQL injection is a prevalent security vulnerability that poses a significant risk to application data. Node.js developers can mitigate this risk by using Knex.js, a promising SQL query builder, to create safer SQL queries. However, note that Knex.js also allows building and running raw SQL queries, in which case, SQL injection is still a security issue as it may flow into the query when concatenating user input.

Knex.js is a powerful SQL query builder for Node.js. It supports transactions, connection pooling, migrations, and seeds, making it a preferred choice for developers seeking to write secure, robust, and scalable SQL queries. Knex.js safeguards against SQL Injection attacks by using parameterized queries and escaping values that are entered into the SQL statements.

Consider the following code snippet that demonstrates how Knex.js protects against SQL injection attacks:

1const knex = require('knex')({
2  client: 'pg',
3  connection: {
4    host : '127.0.0.1',
5    user : 'your_database_user',
6    password : 'your_database_password',
7    database : 'myapp_test'
8  }
9});
10
11// This value is provided by the user and could be malicious
12// such as applying an OR 1=1 SQL injection or other
13// techniques that escape the original context of the query
14// and create a new one
15let userProvidedValue = 'maliciousValue';
16
17knex('users')
18  .where('id', '=', userProvidedValue)
19  .select()
20  .then(rows => {
21    // process result
22  })
23  .catch(err => {
24    // handle error
25  });

In this example, the userProvidedValue is automatically escaped by Knex.js, preventing any potential SQL Injection attack.

As we said before, using Knex.js by itself isn't a complete sandbox and security mitigation against SQL injection. Here's an example of a potentially insecure Knex.js usage that could lead to a SQL injection attack:

1knex.raw(`SELECT * FROM users WHERE id = ${userProvidedValue}`)

In this case, the userProvidedValue isn't escaped or parameterized, making the query susceptible to SQL injection if the userProvidedValue contains malicious SQL code. Snyk can help developers detect such vulnerabilities in their codebase. Snyk scans your code and provides actionable insights to fix security vulnerabilities, including potential SQL injection attacks.

FAQ on SQL injection prevention with Knex.js

What is an SQL injection?

SQL injection is a technique where an attacker inserts malicious SQL code into a query. This intrusion can lead to unauthorized access to sensitive data, data corruption, or even loss of data. Therefore, preventing SQL Injection attacks is crucial for application security.

Is Knex.js immune to SQL injection?

While Knex.js does a great job of mitigating SQL Injection risks with parameterized queries and value escaping, it isn't entirely immune. Developers need to ensure they are using Knex.js correctly and not introducing vulnerabilities.

Is it enough to use Knex.js to prevent SQL injection?

While Knex.js is a powerful tool for preventing SQL injection, it's not a standalone solution. Developers should also adopt other security best practices such as input validation and least privilege principle.

5. Implement rate limiting with fastify-rate-limit

Rate limiting is a crucial security mechanism that protects your web applications against denial of service (DoS) attacks. By controlling the number of requests a client can make to your application within a specific timeframe, you prevent rogue clients from overwhelming your server with a large number of requests, thus ensuring that your application remains available to other legitimate users.

fastify-rate-limit is a plugin for the Fastify web framework that provides an easy-to-use interface for implementing rate limiting in your Node.js applications. The plugin lets you specify the maximum number of requests a client can make within a specific timeframe and the response to send when this limit is exceeded.

Here's a basic example of how you can implement rate limiting in your Node.js application using fastify-rate-limit:

1const fastify = require('fastify')()
2
3fastify.register(require('@fastify/rate-limit'), {
4  // max number of connections during windowMs milliseconds before sending a 429 response
5  max: 100,
6  timeWindow: '1 minute'
7})
8
9fastify.get('/', (req, reply) => {
10  reply.send({ hello: 'world' })
11})
12
13fastify.listen(3000, err => {
14  if (err) throw err
15  console.log('Server listening at http://localhost:3000')
16})

In this code snippet, Fastify's rate limit package is configured to limit each client to 100 requests per minute. If a client exceeds this limit, the server responds with a 429 ("Too Many Requests") status code.

FAQ on rate limiting

Can rate limiting affect the performance of my application?

No, rate limiting improves the performance and availability of your application by preventing it from being overwhelmed by a large number of requests.

What should I do if a legitimate user exceeds the rate limit?

Depending on your application’s needs, you can choose to increase the rate limit, exclude certain IP addresses from rate limiting, or implement a more sophisticated rate limiting strategy that takes into account the user's behavior and reputation.

Can rate limiting prevent all types of denial of service (DoS) attacks?

Rate limiting is effective against DoS attacks that aim to overwhelm your server with a large number of requests. However, it cannot protect against other types of DoS attacks that exploit specific vulnerabilities in your application or server. It is therefore important to implement a comprehensive security strategy that includes rate limiting as well as other security measures.

Why developers should use Snyk for JavaScript security

In this blog post, we have covered five essential Node.js security code snippets that every backend developer should be familiar. We mentioned the importance of secure coding practices such as avoiding SQL injection and how they play a significant role in enhancing application security. With the evolution of technology and the increasing complexity of cyber threats, the need for secure coding cannot be overstressed.

As a developer, it is crucial to leverage robust security tools that can help identify and mitigate potential vulnerabilities in your code. One such tool is Snyk, a powerful developer security platform that offers developers the ability to detect and fix vulnerabilities in code, dependencies, containers, and more. With Snyk, you can continuously monitor your application for security vulnerabilities and receive automatic fix PRs when a new vulnerability is discovered.

1// Install Snyk CLI
2npm install -g snyk
3
4// Then run Snyk to find vulnerabilities
5snyk test

Snyk integrates seamlessly into your IDEs and CI/CD pipeline, enabling you to maintain continuous security throughout your application development lifecycle. It supports a wide range of programming languages, including Node.js, making it a versatile tool for developers across different platforms.

Secure coding is a fundamental practice in software development that every backend developer should prioritize. The use of security tools like Snyk further enhances this practice by providing automated vulnerability detection and remediation capabilities. As we continue to navigate the ever-evolving landscape of cyber threats, these practices and tools will be vital in maintaining the security and integrity of our applications.