Express JS Security Best Practices 2025

Ahmad Sadeddin

CEO at Corgea

Introduction

Express is one of the most popular Node.js frameworks and is used by thousands of APIs and applications globally. In 2025, the security landscape has evolved – from sophisticated injection attacks to relentless bots – so developers must proactively ensure that their Express applications follow security best practices. This guide covers the Express.js security best practices you should implement to protect user data and keep your Node.js services safe.

Keep Express and Dependencies Up-to-Date

One of the foundational best practices is to keep your Express framework and all dependencies updated. Running old, unsupported versions (e.g., Express 3.x) can expose you to unpatched security flaws. Always upgrade to maintained versions (Express 4.x or newer) and apply security patches promptly.

# Check for outdated packages
npm outdated

# Audit and fix known vulnerabilities
npm audit && npm

Never Trust User Input (Validation & Sanitization)

A core principle of web security is: never trust user input. All data coming from clients – query params, request bodies, headers, etc. – should be treated as potentially malicious. Without proper validation and sanitization, user input can lead to severe exploits like SQL injection or Cross-Site Scripting (XSS).

For example, an attacker could submit a <script> tag in a form field to execute malicious JavaScript in other users' browsers (an XSS attack), or craft an SQL snippet in a login field to manipulate database queries (SQL injection).

To prevent these, always validate format and length of inputs and escape or strip dangerous characters. You can use libraries like express-validator to enforce rules easily:

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

app.post('/register', [
  body('username').isAlphanumeric().trim().isLength({ min: 3 }),  // letters/numbers only
  body('email').isEmail().normalizeEmail(),                      // valid email format
  body('password').isLength({ min: 8 })                          // enforce minimum length
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    // Input failed validation
    return res.status(400).json({ errors: errors.array() });
  }
  // Proceed with using the sanitized inputs safely...
});

For database queries, always use parameterized queries or ORM methods to avoid SQL injection. Never directly concatenate user input into an SQL string. For example, using a placeholder ? for user ID in an SQL query and passing values separately ensures the database driver handles escaping:

const userId = req.body.userId;
const query = "SELECT * FROM users WHERE id = ?"; 
db.query(query, [userId], (err, results) => { ... });

By diligently validating and sanitizing all inputs, you close the door on a huge class of vulnerabilities and ensure that user-supplied data cannot harm your Express application.

Use Helmet and Set Security Headers

HTTP response headers can bolster your app's security by instructing browsers to mitigate certain risks. The Helmet middleware is an easy way to set many of these headers in Express. Helmet configures headers like Content-Security-Policy (CSP), X-Content-Type-Options, and more.

By simply adding Helmet to your Express app, you get sensible defaults for many of these:

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

app.use(helmet());  // apply Helmet middleware

Remove or Change the X-Powered-By Header

By default, Express sends:

X-Powered-By: Express

This gives away implementation details. Remove it:

app.disable('x-powered-by');

Or set a misleading value:

app.use((req, res, next) => {
  res.setHeader('X-Powered-By', 'PHP/8.0.0');
  next();
});

Secure Cookies and Session Management

If your Express app uses cookies (for sessions or JWT tokens), it's vital to configure cookies securely to prevent common attacks. First, never use the default session cookie name. Attackers know Express's default (e.g. connect.sid) and can use it to identify your stack. Choose a generic name like "sessionId" instead.

More importantly, always set the proper cookie flags:

  • Secure: Send cookie only over HTTPS

  • HttpOnly: Don't allow JavaScript access to the cookie

  • SameSite: Control cross-site cookie behavior

  • Domain/Path: Restrict cookie scope

  • Expires/Max-Age: Set appropriate expiration

When using Express session middleware, you can configure these easily:

const session = require('express-session');
app.use(session({
  secret: process.env.SESSION_SECRET,  // Use environment variable for secret
  name: 'sessionId',                   // custom cookie name to avoid default fingerprint
  cookie: {
    secure: true,                      // send cookie only over HTTPS
    httpOnly: true,                    // don't allow JS access to the cookie
    sameSite: 'strict',                // strict SameSite policy
    maxAge: 60 * 60 * 1000             // optional: cookie expiration in ms (here, 1 hour)
  }
}));

Implement Strong Authentication & Authorization

Robust authentication and authorization mechanisms are non-negotiable for secure Express apps. Ensure that user login systems are built using secure practices: hash passwords with a strong algorithm, implement multi-factor auth if possible, and use well-tested libraries for managing auth flows.

For password storage, never store plaintext passwords – use bcrypt or a similar secure hash:

const bcrypt = require('bcrypt');
const saltRounds = 10;
const plaintextPwd = req.body.password;

// Hash the password before saving a new user
const hashedPassword = await bcrypt.hash(plaintextPwd, saltRounds);
// Store hashedPassword in the database

This ensures that even if your user database is compromised, the actual passwords are not immediately exposed (bcrypt hashes are computationally expensive to crack).

Enable CSRF Protection

Cross-Site Request Forgery (CSRF) is an attack where malicious websites trick a user's browser into making unintended requests to your Express app (while the user is logged in). If your app relies on cookies for authentication (a common scenario for session-based auth), you should implement CSRF protection on state-changing POST/PUT/DELETE routes.

The typical solution is using CSRF tokens. Each time you render a form, include a hidden input with a token that is unique to the user's session. The server validates this token on form submission, ensuring the request is genuine:

const csrf = require('csurf');
app.use(csrf());  // enable CSRF protection

// In a route that renders a form:
app.get('/account/settings', (req, res) => {
  res.render('settings', { csrfToken: req.csrfToken() });
});

// In the corresponding template (Pug/EJS/HTML):
// <form action="/account/settings" method="POST">
//   <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
//   <!-- other form fields -->
// </form>

Rate Limiting and Brute-Force Protection

To protect against brute-force attacks and abuse, implement rate limiting on your Express endpoints. Brute-force attacks (especially on login pages) involve attackers trying many requests or password guesses in quick succession. A simple way to mitigate this is by using an express-rate-limit middleware that caps how many requests each IP or user can make to a route in a given timeframe.

For example, to limit repeated login attempts:

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

// Limit to 5 login attempts per 15 minutes per IP
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 5,                    // max 5 attempts
  message: "Too many login attempts. Try again later."
});
app.use("/login", loginLimiter);

In this snippet, if a single IP tries to hit the /login route more than 5 times in 15 minutes, they will get HTTP 429 Too Many Requests. You can similarly apply global rate limits to all routes to mitigate basic denial-of-service floods.

Secure File Uploads

If your Express app accepts file uploads (for example, user profile images or documents), it's critical to handle them securely. File uploads can introduce vulnerabilities such as malware uploads, file type confusion, or directory traversal attacks if not properly controlled.

Here are some best practices for file uploads:

  • Limit accepted file types

  • Limit file size

  • Store files safely

  • Sanitize file names

Using a middleware like multer for handling multipart/form-data, you can implement some of these controls:

const multer = require('multer');
const upload = multer({
  dest: 'uploads/', // directory to save files
  limits: { fileSize: 2 * 1024 * 1024 }, // 2 MB file size limit
  fileFilter: (req, file, cb) => {
    // Only accept image files
    if (!file.mimetype.startsWith('image/')) {
      return cb(new Error('Only image files are allowed!'), false);
    }
    cb(null, true);
  }
});

// Usage in a route:
app.post('/profile/photo', upload.single('photo'), (req, res) => {
  res.send('File uploaded successfully');
});

Error Handling and Logging

How your Express app handles errors can impact security. In development, Express will show stack traces on errors, but in production you should never expose stack traces or detailed error messages to users. These can leak implementation details that help attackers (for example, revealing file paths, library versions, or code snippets). Make sure to provide a user-friendly generic error message instead.

You can achieve this by writing a custom error-handling middleware:

app.use((err, req, res, next) => {
  console.error(err.stack);  // log the error stack to server console
  res.status(500).send('Internal Server Error');  // generic message to client
});

Ready be secure?

Harden your software in less than 10 mins'