Secure your Node.js apps against common threats.
In this chapter, we'll explore how to protect your Node.js applications from two of the most common vulnerabilities: injection attacks and XSS (Cross-Site Scripting). We'll cover both foundational concepts and advanced techniques to help you secure your applications effectively.
Use parameterized queries and ORM libraries to prevent SQL injection. Never concatenate user input directly into SQL queries.
const User = require('./models/User');
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Safely query the database using an ORM
const user = await User.findOne({ where: { email } });
if (!user) return res.status(401).json('Invalid credentials');
res.json(user);
} catch (error) {
console.error(error);
res.status(500).json('Internal server error');
}
});
When using NoSQL databases like MongoDB, always validate and sanitize user input. Avoid using raw query strings.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
username: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true }
});
XSS attacks occur when an application outputs untrusted user input without proper sanitization. This can allow attackers to execute scripts in the context of other users.
const express = require('express');
const sanitizeHtml = require('sanitize-html');
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
const cleanBody = sanitizeHtml(buf.toString());
if (cleanBody !== buf.toString()) {
console.error('Malicious input detected');
return res.status(400).json({ error: 'Invalid request' });
}
}
}));
Helmet is a security middleware that helps set essential security headers. It provides multiple security-related middlewares in one package.
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
'default-src': "'self'",
'script-src': ["'self'", "https://example.com"]
}
})
}));
CSP is a security feature that prevents XSS by specifying which sources of content are allowed to load on your web pages.
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' https://example.com;");
next();
});
In this example, we'll create a secure login endpoint using Express.js, Helmet.js, and sanitization libraries.
const express = require('express');
const helmet = require('helmet');
const sanitizeHtml = require('sanitize-html');
const app = express();
app.use(helmet());
app.use(express.json({
verify: (req, res, buf) => {
const cleanBody = sanitizeHtml(buf.toString());
if (cleanBody !== buf.toString()) {
console.error('Malicious input detected');
return res.status(400).json({ error: 'Invalid request' });
}
}
}));
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Validate and sanitize input
const cleanEmail = sanitizeHtml(email);
// Use parameterized queries with an ORM
const user = await User.findOne({ where: { email: cleanEmail } });
if (!user) return res.status(401).json('Invalid credentials');
res.json(user);
} catch (error) {
console.error(error);
res.status(500).json('Internal server error');
}
});
Rate limiting and brute-force protection are critical components of building secure Node.js applications. These techniques help prevent malicious attacks such as API abuse, password cracking, and resource exhaustion.
Rate limiting involves tracking the number of requests from a single client within a specified time window. If the limit is exceeded, further requests are either delayed or blocked.
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
For applications handling high traffic, a distributed rate limiting solution using Redis is recommended.
const redisStore = require('rate-limit-redis');
const limiterRedis = rateLimit({
store: redisStore,
windowMs: 60 * 1000, // 1 minute
max: 50,
message: 'Too many requests from this IP, please try again in a minute.',
});
Protecting against brute-force attacks involves monitoring login attempts and implementing measures to block suspicious activity.
const express = require('express');
const limiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 5,
message: 'Too many login attempts. Try again in 5 minutes.'
});
const app = express();
// API endpoint rate limiting
app.use('/api/*', limiter);
// Login brute-force protection
app.post('/login', limiter, (req, res) => {
// Your authentication logic here
});
Welcome to Dependency & Secrets Management! In this chapter, you'll learn how to keep your Node.js applications secure by managing dependencies effectively and safeguarding sensitive information like API keys. Let's get started!
In Node.js, dependencies are managed through package.json
. These include:
{
"name": "my-app",
"version": "1.0.0",
"devDependencies": {
"eslint": "^8.53.0"
},
"dependencies": {
"express": "^4.18.2"
}
}
Regularly updating dependencies is crucial for security. Use these tools:
npm audit
to find vulnerabilities$ npm audit fix
$ npm run dependabot-update
The npm audit
tool checks your project's dependencies for known security vulnerabilities. It provides actionable recommendations to fix issues.
$ npm audit
# Output will show detected vulnerabilities and suggested fixes
GitHub's Dependabot automatically updates your dependencies and opens pull requests for approval. To enable it:
// Create or modify .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: daily
The dotenv
package helps manage environment variables. Create a .env file to store secrets like API keys.
# Example .env
DB_PASSWORD=supersecret123
API_KEY=abc123xyz
Load environment variables in your application:
require('dotenv').config();
const dbPassword = process.env.DB_PASSWORD;
dotenv-ignore
if using Gitprocess.env
for accessing environment variablesProper dependency and secret management is critical for applications like e-commerce platforms, APIs, and SaaS tools. By following best practices, you protect user data and maintain trust.
In this chapter, you've learned about managing dependencies with npm audit and Dependabot, safeguarding secrets with dotenv, and following best practices for security. By applying these techniques, you'll build more secure Node.js applications.
Question 1 of 15