Node.js Security Best Practices

Security protects your users, your data, and your reputation. A secure app uses many small defenses together. This lesson collects the most useful habits for Node.js and Express projects.

Think in Layers

A castle uses a moat, walls, gates, and guards. One defense can fail, so several layers work together. Apply the same idea to your app.

 Attacker
    |
    v
+-------------------------------+
| HTTPS and secure headers      |
|  +-------------------------+  |
|  | Rate limits and CORS    |  |
|  |  +-------------------+  |  |
|  |  | Validation        |  |  |
|  |  |  +-------------+  |  |  |
|  |  |  | Safe data   |  |  |  |
|  |  |  +-------------+  |  |  |
|  |  +-------------------+  |  |
|  +-------------------------+  |
+-------------------------------+

Use Helmet for Secure Headers

HTTP headers instruct the browser how to treat your pages. Helmet sets many safe headers with one line.

npm install helmet
const helmet = require("helmet");
app.use(helmet());

Helmet hides the server technology, blocks content sniffing, and sets a content security policy.

Always Use HTTPS

HTTPS encrypts data between the browser and the server. Without it, anyone on the same network can read passwords. Hosting providers offer free certificates. Redirect all HTTP traffic to HTTPS.

Guard Secrets

  • Store keys, passwords, and tokens in environment variables.
  • Add the .env file to .gitignore.
  • Use different secrets for development and production.
  • Rotate a secret right away after any leak.
const secret = process.env.JWT_SECRET;
if (!secret) {
  throw new Error("JWT_SECRET is missing");
}

Prevent Injection Attacks

Injection happens when attacker text becomes part of a command. The database or system runs that text as code.

Database Injection

Never glue user text into a query. Use parameters.

// Dangerous
db.query("SELECT * FROM users WHERE email = '" + email + "'");

// Safe
db.query("SELECT * FROM users WHERE email = ?", [email]);

MongoDB Injection

An attacker can send an object such as { "$ne": null } instead of text. Check that the input is a string before you query.

if (typeof email !== "string") {
  return res.status(400).send("Invalid input");
}

Command Injection

Avoid exec with user text. Use execFile with a fixed command and an argument list.

Stop Cross-Site Scripting

Cross-site scripting, called XSS, places harmful scripts into your pages. Escape all user text before you display it. Template engines escape output by default. Avoid inserting raw HTML from users.

Protect Cookies and Sessions

res.cookie("session", token, {
  httpOnly: true,   // scripts cannot read it
  secure: true,     // sent over HTTPS only
  sameSite: "strict",
  maxAge: 60 * 60 * 1000
});

Control Access

  • Check login status on every private route.
  • Check ownership. A user may edit only their own records.
  • Give each role the minimum rights it needs.
  • Return generic messages for failed logins.

Limit Request Size

app.use(express.json({ limit: "100kb" }));

A size limit blocks attackers who send giant bodies to exhaust memory.

Keep Packages Safe

Every library you install is code that runs on your server. Vulnerabilities appear in popular packages over time.

npm audit
npm audit fix
npm outdated
  • Install only packages you trust and need.
  • Commit the lock file so installs stay predictable.
  • Update dependencies on a regular schedule.

Handle Errors Quietly

Detailed errors help attackers. Show visitors a short message and write the details to a private log.

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: "Something went wrong." });
});

Security Checklist

  • Helmet enabled.
  • HTTPS enforced.
  • Passwords hashed with bcrypt.
  • Inputs validated on the server.
  • Rate limits on login routes.
  • CORS limited to trusted origins.
  • Secrets stored outside the code.
  • Dependencies audited.

Practice Tasks

  • Add Helmet to a sample app and inspect the response headers.
  • Run npm audit on one of your projects and read the report.
  • Rewrite a string-built query into a parameterized query.