Blog

Is My Express App Secure?

Key Takeaways

  • Keep Express, Node.js, and dependencies on supported releases, and test upgrades before deploying them.
  • Use Helmet for security headers, but design your Content Security Policy and application-level defenses separately.
  • Require HTTPS externally and trust forwarded headers only from your actual reverse proxies.
  • Never use express-session’s default MemoryStore in production; configure a shared store, secure cookies, expiration, and an externally managed secret.
  • Protect authorization, cookie-authenticated requests, input handling, and public endpoints—not just the framework configuration.

Your Express app isn’t secure just because it uses Express. You need deliberate controls around transport, sessions, authentication, application logic, and dependencies. The Express production security guidance is a starting point, not a certification that your application is safe.

1. Keep Express and Node.js Supported

As of October 17, 2026, Express 5 is the current major line, but Express lists both Express 5 and Express 4 as supported. Only the latest release within a supported major line receives support. Check the official Express support table rather than assuming every older major is unsupported.

Express 5 requires Node.js 18 or higher, but that minimum isn’t a production recommendation. Node.js 18 and 20 are already end-of-life; Node.js 24 and 22 are supported LTS lines on the Node.js release schedule. Choose a supported LTS runtime and keep its patches current. Our guide to choosing a Node.js version explains that distinction.

An Express 4-to-5 upgrade can change application behavior, including route matching and request parsing. Follow the Express 5 migration guide, then test routing, authentication, error paths, and third-party middleware. Treat a major upgrade as a tested change, not an automatic security fix.

2. Use Helmet as a Header Baseline

Helmet sets security-related HTTP response headers. Its defaults include Content Security Policy, or CSP, and X-Content-Type-Options: nosniff, which mitigates MIME-type sniffing.

import helmet from "helmet";

// Fragment: app is your existing Express instance.
// Register before routes that send responses.
app.use(helmet());

CSP tells browsers which resources a document may load. Its policy must match your scripts, styles, embedded content, and other application requirements. For example, you can test a policy that disallows framing:

// Fragment: replace the helmet() registration above in staging.
app.use(helmet({
  contentSecurityPolicy: {
    reportOnly: true,
    directives: {
      frameAncestors: ["'none'"]
    }
  }
}));

Helmet merges these directives with its default policy. Report-only CSP reports violations without enforcing the policy; exercise your application and inspect browser diagnostics before removing reportOnly to enforce it. Don’t leave report-only mode as your final protection.

Helmet does not prevent cross-site scripting, or XSS, by itself. You still need safe templating, context-appropriate output encoding, and safe browser APIs, as described in OWASP’s XSS prevention guidance. See Helmet and Express.js for more on header configuration.

3. Require HTTPS and Define the Proxy Boundary

TLS termination is where an encrypted HTTPS connection ends. That can happen in Node.js, but you can also terminate TLS at an ingress, load balancer, or hosting platform and forward requests to Express. For a self-hosted proxy, Caddy’s automatic HTTPS documentation covers certificate issuance, renewal, and HTTP-to-HTTPS redirects.

Browser -- HTTPS --> Trusted ingress / reverse proxy
                              |
                    restricted upstream connection
                              |
                              v
                           Express --> Private session store

Trust forwarded headers only across the known proxy boundary.

Require HTTPS on public endpoints, automate certificate renewal, and monitor renewal failures. Keep the upstream Express listener private. Use TLS on upstream links where your network’s trust requirements call for it, and don’t expose databases, session stores, or debug endpoints publicly.

The Express trust proxy setting affects how Express interprets forwarded protocol and client-IP information. Match it to known proxy addresses or your verified network topology. Your trusted edge must sanitize forwarded headers; blindly enabling trust proxy can let clients spoof values used by security-sensitive application logic.

4. Configure Session Cookies and Storage Deliberately

Choose a maintained store that fits your infrastructure from the express-session compatible store documentation. A store adapter does not secure its backing database: configure expiration, restricted network access, least-privilege credentials, and encrypted connections.

The following configuration fragment uses connect-redis. Install its dependencies in your application:

npm install express-session redis connect-redis

Supply SESSION_SECRET and REDIS_URL through deployment configuration or your secrets manager. Generate the secret from at least 32 bytes of cryptographically random data; a long human-chosen password is not equivalent. Follow the Redis client connection guidance for TLS and connection options.

import session from "express-session";
import { RedisStore } from "connect-redis";
import { createClient } from "redis";

// Configuration fragment for an existing ESM application.
// app is your Express instance; add this before protected routes.
const secret = process.env.SESSION_SECRET;
const redisUrl = process.env.REDIS_URL;
if (!secret || !redisUrl) {
  throw new Error("Session configuration is missing");
}

const redis = createClient({ url: redisUrl });
redis.on("error", () => {
  console.error("Session store connection error");
});
try {
  await redis.connect();
} catch {
  throw new Error("Session store startup failed");
}

// ONLY for a trusted, same-host proxy connecting over loopback.
// Keep the Express listener private; adapt for other topologies.
app.set("trust proxy", "loopback");

app.use(session({
  name: "__Host-sid",
  secret,
  store: new RedisStore({ client: redis, prefix: "myapp:session:" }),
  resave: false,
  saveUninitialized: false,
  rolling: true,
  cookie: {
    secure: true,
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 30 * 60 * 1000
  }
}));

This is session configuration, not a complete authentication server. Add bounded store-operation deadlines, graceful shutdown, and application-specific login and authorization logic. Don’t fall back to unauthenticated access when the store fails.

Secure restricts cookie transmission to secure connections, and HttpOnly prevents JavaScript from reading the cookie through document.cookie. Neither stops an XSS payload from making authenticated requests. The __Host- prefix requires Secure, Path=/, and no Domain attribute in supporting browsers; see the Set-Cookie reference.

The example chooses a 30-minute rolling cookie lifetime, not a universal timeout. connect-redis uses the session cookie’s expiration to determine the stored session’s time-to-live. Also enforce an absolute session lifetime server-side so continued activity cannot keep a session alive indefinitely, following OWASP’s session management guidance.

With saveUninitialized: false, an untouched new session isn’t stored or given a cookie. Behind a TLS-terminating proxy, secure cookies depend on Express correctly recognizing HTTPS through that trusted proxy. Test cookie issuance through the real deployment path.

5. Protect Authentication, Requests, and Untrusted Data

Authenticate users and authorize every resource

If you manage passwords, use a maintained password-hashing implementation with an appropriate adaptive algorithm, such as Argon2id, following OWASP’s password storage recommendations. Never store plaintext passwords or substitute a fast general-purpose hash.

Regenerate the session ID after login or privilege changes, and destroy the server-side session on logout. Invalidate affected sessions after account compromise. Authentication establishes identity; authorization must check permissions on every protected request, including ownership and tenant boundaries for the specific resource.

Cross-site request forgery, or CSRF, tricks a browser into submitting an authenticated request the user didn’t intend. Protect cookie-authenticated state-changing requests with a server-validated CSRF token or an appropriate equivalent design, and validate request origins where applicable. Never make GET requests perform state-changing actions.

SameSite=Lax is a useful layer, not a general replacement for CSRF protection. It permits cookies on some top-level navigations, and sibling subdomains can still be considered same-site. Cross-site integrations may require SameSite=None; those cookies require Secure and need deliberate CSRF defenses. Use the OWASP CSRF prevention guide to evaluate your request flows.

Validate input and keep data out of executable syntax

Validate types, lengths, allowed fields, and business rules at the server boundary. Use escaped template output for untrusted text, and sanitize rich HTML only when your application intentionally accepts it.

Use parameterized database queries rather than concatenating user input into SQL. Don’t pass arbitrary request objects into database filters or build shell commands from request strings. Validation and injection prevention solve different parts of the problem; you need both.

6. Limit Abuse and Keep Errors Private

Bound request size and expensive work

Set request-body limits that fit each endpoint. For example, a small JSON endpoint can use express.json({ limit: '16kb' }); that is an application-selected limit, not a recommendation for file uploads. The Express parser reference documents the available options.

Apply separate upload limits, request timeouts, concurrency controls, and rate limits to costly public endpoints. Protect login and password-reset flows against brute force, and consider both account-based and client-IP-based controls. These concerns are covered in the OWASP Node.js security checklist. Your limiter’s client-IP handling must agree with your proxy configuration.

Place these controls deliberately in the request pipeline; our guide to writing Express middleware explains middleware composition.

Return safe errors and redact logs

Set NODE_ENV=production and use centralized error-handling middleware after your routes. Express’s default production error handler omits stack traces; custom handlers must avoid exposing stacks, secrets, or internal details too. Follow the official error-handling guide and our Express error-handling patterns.

Log enough context to investigate failures, but redact passwords, authorization tokens, cookies, session identifiers, and connection strings. Don’t dump entire request objects or bodies. OWASP’s logging guidance explains which sensitive values to exclude.

7. Make Dependency Security a Workflow

Commit package-lock.json, review dependency changes, and use npm ci in CI. It installs from the lockfile and fails when package.json and the lockfile disagree rather than rewriting the lockfile.

For a repository with a configured test script, a minimal CI shell step is:

set -eu
npm ci --include=dev
npm audit --package-lock-only --include=dev --audit-level=high
npm test

npm audit checks for known dependency vulnerabilities. Here, --audit-level=high makes high and critical findings fail the audit; it does not hide lower-severity findings. Include build and development dependencies in review, not just packages shipped in the server image.

  1. Investigate the advisory, affected dependency path, and whether your application can reach the vulnerable behavior.
  2. Apply a compatible fix where possible; test and review major-version upgrades separately.
  3. Review lockfile changes, new dependencies, and package install scripts before merging.
  4. Track unresolved findings with an owner, mitigation, and review date.

Don’t blindly run npm audit fix --force: it can install changes outside your declared dependency ranges, including major upgrades. A clean audit is not proof against malicious packages, compromised maintainers, or application bugs. Pair advisory checks with the Node.js supply-chain security recommendations.

Production Checklist

  • Test HTTP-to-HTTPS behavior, certificate renewal monitoring, and rejection of direct access to private services.
  • Verify cookie attributes after login through the real ingress, then test logout and server-side expiration.
  • Attempt cross-account and cross-tenant resource access; don’t test only successful authorization.
  • Exercise missing CSRF tokens, oversized bodies, repeated login attempts, and session-store outages.
  • Inspect client errors and logs for leaked secrets, and confirm dependency findings have a remediation process.

FAQ

Does Helmet prevent XSS in Express?

No. Helmet sets security headers, including a configurable Content Security Policy, but it does not make unsafe templates or browser code safe. You still need context-appropriate output encoding and safe handling of untrusted HTML.

How should I configure express-session cookies for HTTPS?

Set secure and httpOnly to true, choose SameSite for your application’s request flows, and configure an explicit lifetime. Behind a TLS-terminating proxy, configure trust proxy for the actual trusted proxy so Express recognizes HTTPS; otherwise secure cookies may not be issued.

Can I use express-session MemoryStore in production?

No. MemoryStore is intended for development and debugging, can grow memory usage, and does not scale beyond one process. Use a maintained production store with expiration and appropriate infrastructure security.

Should TLS terminate in Express or at a reverse proxy?

Either architecture can work. A trusted ingress or reverse proxy can manage certificates and HTTPS redirects, while Express remains on a restricted upstream connection. Configure proxy trust and upstream encryption to match that deployment.

Does npm audit guarantee that my dependencies are secure?

No. npm audit reports known vulnerabilities; it does not prove packages are free of malicious code or undiscovered flaws. Review dependency changes, maintain a committed lockfile, and combine audits with application security testing.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me