skip to content
logo
Table of Contents

Node.js Security Best Practices

Securing a Node.js application involves protecting against injections, handling authentication securely, managing dependencies, and securing API endpoints.
This guide covers:

  • Preventing SQL & NoSQL injections
  • Securing authentication and session management
  • Avoiding prototype pollution vulnerabilities
  • Protecting API endpoints from abuse

1. Validate and Sanitize User Input

Unvalidated user input can lead to injections, XSS, and prototype pollution attacks.

Bad: Directly Using User Input in Queries

app.get("/user", async (req, res) => {
const user = await db.query(`SELECT * FROM users WHERE id = ${req.query.id}`);
res.json(user);
});

Fix: Use Parameterized Queries

app.get("/user", async (req, res) => {
const user = await db.query("SELECT \* FROM users WHERE id = $1", [
req.query.id,
]);
res.json(user);
});

Fix: Use Input Validation with Zod

import { z } from "zod";
const userSchema = z.object({
id: z.string().uuid(),
});
const validatedData = userSchema.parse(req.query);

2. Prevent Prototype Pollution Attacks

Attackers can inject properties into JavaScript prototypes, causing unexpected behavior.

Vulnerable Code

const userPreferences = JSON.parse(req.body);
Object.assign({}, userPreferences);

Fix: Use Object.create(null)

const safeObject = Object.create(null);
Object.assign(safeObject, userPreferences);

3. Secure Authentication and Sessions

Bad: Storing JWT in Local Storage

localStorage.setItem("token", token);

Fix: Use Secure HTTP-Only Cookies

// Server-side (Express.js)
app.post("/login", (req, res) => {
res.cookie("auth_token", "secure-token", {
httpOnly: true,
secure: true,
sameSite: "Strict",
});
res.send("Logged in");
});

Fix: Rotate JWTs to Prevent Token Replay

const refreshToken = jwt.sign({ userId }, secretKey, { expiresIn: "7d" });

4. Secure API Endpoints with Rate Limiting

Rate limiting prevents brute-force attacks and API abuse.

import rateLimit from "express-rate-limit";
const limiter = rateLimit({ windowMs: 15 _ 60 _ 1000, max: 100 });
app.use("/api/", limiter);

5. Prevent Directory Traversal Attacks

Allowing unsanitized file paths can let attackers access system files.

Vulnerable Code

app.get("/file", (req, res) => {
res.sendFile(\_\_dirname + "/uploads/" + req.query.filename);
});

Fix: Sanitize File Paths

import path from "path";
app.get("/file", (req, res) => {
const safePath = path.join(\_\_dirname, "uploads", path.basename(req.query.filename));
res.sendFile(safePath);
});

6. Keep Dependencies Updated and Secure

Vulnerable dependencies introduce security flaws.

Check for Security Issues

Terminal window
npm audit

Fix Vulnerabilities

Terminal window
npm audit fix --force

7. Set Secure HTTP Headers

Use Helmet.js to enforce secure HTTP headers.

import helmet from "helmet";
app.use(helmet());

This prevents:

  • Clickjacking attacks (X-Frame-Options)
  • Cross-site scripting (X-XSS-Protection)
  • Content sniffing attacks (X-Content-Type-Options)

8. Avoid Exposing Sensitive Data in Errors

Bad: Returning Detailed Error Messages

app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});

Fix: Use Generic Error Responses

app.use((err, req, res, next) => {
res.status(500).json({ error: "Internal Server Error" });
});

Conclusion

  • Validate all user input to prevent SQL & NoSQL injections.
  • Prevent prototype pollution attacks by securing object assignments.
  • Use HTTP-only cookies for authentication instead of storing JWTs in local storage.
  • Secure API endpoints with rate limiting and proper authorization.
  • Sanitize file paths to prevent directory traversal attacks.
  • Keep dependencies updated and use npm audit to check for vulnerabilities.

Security is a continuous process — implement these safeguards to reduce risks and protect data.