Best practices for securing Node.js APIs

Mayank C
Tech Tonic

--

In this beginner article, we’ll go over some best practices that are commonly followed in securing Node.js APIs. Let’s get started.

1 Validate and sanitize user input

Validating and sanitizing user input is a crucial security practice to prevent common web attacks such as SQL injection and cross-site scripting (XSS). Node.js APIs receive user input through various sources like request bodies, query parameters, and headers. If this input is not validated and sanitized, it can lead to severe security vulnerabilities.

Why is it important?

  • Prevents SQL injection attacks by ensuring that user input does not contain malicious SQL code.
  • Prevents XSS attacks by ensuring that user input does not contain malicious JavaScript code.
  • Prevents command injection attacks by ensuring that user input does not contain malicious system commands.

How to implement?

  • Use a validation library like Joi or Express-Validator to define validation rules for user input.
  • Use a sanitization library like DOMPurify or strip-js to remove malicious code from user input.
  • Implement input validation and sanitization at the earliest point of input processing, typically in the request handler or middleware.

Code sample:

const express = require('express');
const { validate, validationResult } = require('express-validator');

const app = express();

app.post('/users', [
validate({
body: {
name: {
isLength: {
errorMessage: 'Name should be at least 3 chars long',
options: { min: 3 }
}
},
email: {
isEmail: {
errorMessage: 'Invalid email'
}
}
}
})
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Input is valid, proceed with processing
});

In this example, we use Express-Validator to define validation rules for the name and email fields in the request body. If the input is invalid, we return a 400 error response with the validation errors.

Another code sample:

const express = require('express');
const DOMPurify = require('dompurify');

const app = express();

app.post('/comment', (req, res) => {
const userInput = req.body.comment;
const sanitizedInput = DOMPurify.sanitize(userInput);

// Use the sanitized input in your application
console.log(sanitizedInput);
// ...
});

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

In this example:

  • Receive user input from the comment field in the request body.
  • Pass the user input to the DOMPurify.sanitize() function, which removes any malicious code (such as JavaScript tags or attributes) from the input.
  • Use the sanitized input in our application, which prevents any potential XSS attacks.

Note: Sanitization should be used in addition to validation, not as a replacement. Validation ensures that the input meets the expected format and rules, while sanitization ensures that the input does not contain malicious code.

2 Use HTTPS and TLS encryption

Using HTTPS and TLS encryption is crucial for securing Node.js APIs. HTTPS (Hypertext Transfer Protocol Secure) is an extension of the HTTP protocol that adds an extra layer of security by encrypting data in transit. TLS (Transport Layer Security) is the encryption protocol used by HTTPS to ensure that data remains confidential and tamper-proof.

Why is it important?

  • Encrypts data in transit, preventing eavesdropping and interception.
  • Authenticates the identity of the server, preventing man-in-the-middle attacks.
  • Ensures data integrity, preventing tampering and modification.

How to implement?

  • Obtain an SSL/TLS certificate from a trusted certificate authority (CA).
  • Configure your Node.js server to use the SSL/TLS certificate.
  • Enable HTTPS and TLS encryption for all API endpoints.

Code Sample:

const https = require('https');
const fs = require('fs');

const options = {
key: fs.readFileSync('path/to/ssl/key.pem'),
cert: fs.readFileSync('path/to/ssl/cert.pem')
};

https.createServer(options, (req, res) => {
// Handle incoming requests
}).listen(443, () => {
console.log('Server listening on port 443');
});

In this example:

  • Create an HTTPS server using the https module.
  • Pass the SSL/TLS certificate and key files to the options object.
  • Create a server that listens on port 443 (the default HTTPS port).

Note: Make sure to replace path/to/ssl/key.pem and path/to/ssl/cert.pem with the actual file paths to your SSL/TLS certificate and key.

3 Implement rate limiting and IP blocking

Implementing rate limiting and IP blocking is essential to prevent denial-of-service (DoS) and brute-force attacks on your Node.js API. Rate limiting restricts the number of requests from a single IP address within a certain time frame, while IP blocking blocks requests from specific IP addresses that exceed the rate limit or exhibit malicious behavior.

Why is it important?

  • Prevents denial-of-service (DoS) attacks by limiting the number of requests from a single IP address.
  • Prevents brute-force attacks by limiting the number of requests to sensitive endpoints (e.g., login).
  • Blocks malicious IP addresses that exceed the rate limit or exhibit malicious behavior.

How to implement?

  • Use a rate limiting library like express-rate-limit or limiter to limit requests per IP address.
  • Configure rate limiting rules for specific endpoints or globally.
  • Use an IP blocking library like express-ip-blocker to block malicious IP addresses.
  • Store blocked IP addresses in a database or cache for efficient lookup.

Code Sample:

const express = require('express');
const rateLimit = require('express-rate-limit');

const app = express();

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
delayMs: 0 // Disable delaying - full speed until the max limit is reached
});

app.use('/api', limiter); // Apply rate limiting to /api endpoints

const blockedIPs = ['192.168.1.1', '172.16.1.1']; // Store blocked IP addresses

app.use((req, res, next) => {
if (blockedIPs.includes(req.ip)) {
return res.status(403).send('Forbidden');
}
next();
});

In this example:

  • Create a rate limiter using express-rate-limit and apply it to the /api endpoints.
  • Store blocked IP addresses in the blockedIPs array.
  • Use a middleware function to check if the requesting IP address is blocked, and return a 403 Forbidden response if it is.

By implementing rate limiting and IP blocking, you can prevent denial-of-service and brute-force attacks, and ensure that your Node.js API remains secure and responsive.

4 Use secure password storage

Using secure password storage is crucial to protect user passwords and prevent unauthorized access to your Node.js API. Secure password storage involves hashing and salting passwords to make them unreadable and resistant to reverse engineering.

Why is it important?

  • Protects user passwords from unauthorized access and password cracking attacks.
  • Prevents password exposure in case of a data breach.
  • Complies with security standards and regulations (e.g., OWASP, PCI-DSS).

How to implement?

  • Use a password hashing library like bcrypt or argon2 to hash and salt passwords.
  • Use a sufficient work factor (e.g., 10–12) to slow down hashing and make it more resistant to brute-force attacks.
  • Store the hashed password in your database, not the original password.
  • When authenticating, hash the input password and compare it with the stored hash.

Code Sample:

const bcrypt = require('bcrypt');

// Hash a password
const password = 'my_secret_password';
const saltRounds = 10;
bcrypt.hash(password, saltRounds, (err, hash) => {
// Store the hash in your database
console.log(hash);
});

// Authenticate a user
const inputPassword = 'my_secret_password';
const storedHash = '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'; // Retrieved from database
bcrypt.compare(inputPassword, storedHash, (err, result) => {
if (result) {
console.log('Authenticated!');
} else {
console.log('Authentication failed');
}
});

In this example:

  • Use bcrypt to hash a password with a sufficient work factor (10).
  • Store the hashed password in the database.
  • When authenticating, hash the input password and compare it with the stored hash using bcrypt.compare().

By using secure password storage, you can protect your users’ passwords and prevent unauthorized access to your Node.js API.

5 Use a web application firewall (WAF)

Using a Web Application Firewall (WAF) is essential to protect your Node.js API from common web attacks and vulnerabilities. A WAF acts as a barrier between your application and the internet, filtering incoming traffic to detect and prevent malicious requests.

Why is it important?

  • Protects against common web attacks (e.g., SQL injection, cross-site scripting, cross-site request forgery).
  • Detects and prevents vulnerability exploits (e.g., OWASP Top 10).
  • Reduces the risk of security breaches and data exposure.
  • Complies with security standards and regulations (e.g., PCI-DSS, HIPAA).

How to implement?

  • Choose a WAF solution (e.g., AWS WAF, Google Cloud Armor, Cloudflare WAF).
  • Configure the WAF to detect and prevent common web attacks and vulnerabilities.
  • Integrate the WAF with your Node.js API (e.g., using a reverse proxy).
  • Monitor WAF logs and analytics to stay informed about potential security threats.

Code Sample:

const express = require('express');
const app = express();

// Integrate with Cloudflare WAF (example)
const cloudflare = require('cloudflare-waf');

app.use(cloudflare.wafMiddleware({
// Configure WAF rules and settings
rules: [
{
action: 'block',
conditions: [
{
src: 'ip',
value: '192.168.1.1'
}
]
}
]
}));

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

In this example:

  • Integrate the Cloudflare WAF middleware with our Express.js application.
  • Configure a WAF rule to block requests from a specific IP address (192.168.1.1).

By using a WAF, you can significantly improve the security of your Node.js API and protect it from common web attacks and vulnerabilities.

6 Use JSON Web Tokens (JWTs) for authentication

Using JSON Web Tokens (JWTs) for authentication is a widely adopted best practice in Node.js API development. JWTs provide a secure way to authenticate users and protect API endpoints.

Why is it important?

  • Provides secure authentication and authorization
  • Stateless authentication (no session management required)
  • Scalable and efficient
  • Supports multiple authentication sources (e.g., username/password, social media, tokens)

How to implement?

  • Choose a JWT library (e.g., jsonwebtoken, jwt-js)
  • Generate a secret key or private key for signing JWTs
  • Define a user authentication process (e.g., login, register)
  • Generate a JWT upon successful authentication
  • Verify and decode JWT on each API request
  • Use middleware to authenticate and authorize API endpoints

Code Sample:

const jwt = require('jsonwebtoken');

// Generate a secret key
const secretKey = 'my_secret_key';

// User authentication process (example)
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Authenticate user
const user = authenticateUser(username, password);
if (user) {
// Generate JWT
const token = jwt.sign(user, secretKey, { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).send('Invalid credentials');
}
});

// Verify and decode JWT on each API request (example)
app.use((req, res, next) => {
const token = req.header('Authorization');
if (token) {
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
res.status(401).send('Invalid token');
} else {
req.user = decoded;
next();
}
});
} else {
res.status(401).send('No token provided');
}
});

In this example:

  • Generate a secret key for signing JWTs
  • Define a user authentication process (login)
  • Generate a JWT upon successful authentication
  • Verify and decode JWT on each API request using middleware

By using JWTs for authentication, you can provide a secure and scalable authentication mechanism for your Node.js API.

7 Implement CORS security

Implementing CORS (Cross-Origin Resource Sharing) security is essential to prevent cross-site request forgery (CSRF) attacks and ensure that your Node.js API only accepts requests from authorized origins.

Why is it important?

  • Prevents CSRF attacks by only allowing requests from authorized origins
  • Restricts access to your API from unauthorized domains
  • Complies with security standards and regulations (e.g., OWASP, PCI-DSS)

How to implement?

  • Use a CORS middleware library (e.g., cors, express-cors)
  • Configure CORS options to specify allowed origins, methods, and headers
  • Enable CORS for specific routes or globally
  • Validate and sanitize requests to prevent CSRF attacks

Code Sample:

const express = require('express');
const cors = require('cors');

const app = express();

const corsOptions = {
origin: ['https://example.com', 'https://another-example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
headers: ['Content-Type', 'Authorization']
};

app.use(cors(corsOptions));

// Enable CORS for a specific route
app.get('/api/data', cors(), (req, res) => {
res.json({ data: 'Hello World' });
});

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

In this example:

  • Use the cors middleware library to enable CORS
  • Configure CORS options to specify allowed origins, methods, and headers
  • Enable CORS globally for all routes
  • Also enable CORS for a specific route (/api/data)

By implementing CORS security, you can prevent CSRF attacks and ensure that your Node.js API only accepts requests from authorized origins.

Thanks for reading this article!

--

--