Application Security

Express and Node.js Security Hardening

Practical security hardening for Express.js applications covering middleware, input validation, and production deployment.

James
Senior Security Engineer
4 min read

Express is minimal by design. It does not include security features out of the box, which means every protection is something you must add explicitly. That minimalism is a strength for flexibility, but a weakness for security. This guide covers the essential hardening steps for production Express applications.

Helmet: Security Headers in One Line

Helmet sets essential HTTP security headers:

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

app.use(helmet());

Helmet enables these headers by default:

  • Content-Security-Policy
  • Cross-Origin-Opener-Policy
  • Cross-Origin-Resource-Policy
  • Origin-Agent-Cluster
  • Referrer-Policy
  • Strict-Transport-Security
  • X-Content-Type-Options
  • X-DNS-Prefetch-Control
  • X-Download-Options
  • X-Frame-Options
  • X-Permitted-Cross-Domain-Policies
  • X-Powered-By (removed)

Customize the CSP for your application:

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.yourapp.com"],
    },
  },
}));

Input Validation

Express does not validate input. Every route needs explicit validation. Use a validation library like Joi or express-validator:

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

app.post('/api/users',
  body('email').isEmail().normalizeEmail(),
  body('name').trim().isLength({ min: 1, max: 100 }).escape(),
  body('role').isIn(['user', 'editor']),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Process validated input
  }
);

Validate everything: body, query parameters, URL parameters, and headers. Attackers target all of these.

Prototype Pollution Prevention

Node.js objects are vulnerable to prototype pollution through __proto__, constructor, and prototype properties in user input:

// Vulnerable to prototype pollution
const config = Object.assign({}, defaultConfig, req.body);

// Safe: use a validated schema
const config = {
  theme: req.body.theme || 'default',
  language: req.body.language || 'en',
};

Use Object.create(null) for dictionary objects, and validate input shapes before merging.

Rate Limiting

Without rate limiting, your API is open to brute force attacks, credential stuffing, and resource exhaustion:

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

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window per IP
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', limiter);

// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
});

app.use('/api/auth/', authLimiter);

For distributed applications, use a Redis-backed rate limiter instead of the default in-memory store.

Session Security

If you use sessions (not token-based auth), configure them securely:

const session = require('express-session');
const RedisStore = require('connect-redis').default;

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  name: 'sessionId', // Don't use the default 'connect.sid'
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,       // HTTPS only
    httpOnly: true,     // Not accessible via JavaScript
    maxAge: 3600000,    // 1 hour
    sameSite: 'strict', // CSRF protection
  },
}));

Never use the in-memory session store in production. It leaks memory and does not work with multiple server instances.

CORS Configuration

Configure CORS explicitly. Never use origin: '*' with credentials:

const cors = require('cors');

app.use(cors({
  origin: ['https://yourapp.com', 'https://www.yourapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400, // Cache preflight for 24 hours
}));

Error Handling

Never expose stack traces or internal error details to clients:

// Production error handler
app.use((err, req, res, next) => {
  console.error(err.stack); // Log full error server-side

  // Send generic error to client
  res.status(err.status || 500).json({
    error: {
      message: process.env.NODE_ENV === 'production'
        ? 'Internal server error'
        : err.message,
    },
  });
});

Body Parsing Limits

Set limits on request body size to prevent denial-of-service attacks:

app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

10KB is generous for most JSON APIs. Adjust based on your needs, but always set a limit.

Production Deployment

Environment Variables

Never hardcode secrets:

// Bad
const dbPassword = 'supersecret123';

// Good
const dbPassword = process.env.DB_PASSWORD;
if (!dbPassword) {
  throw new Error('DB_PASSWORD environment variable is required');
}

Process Management

Run Express behind a reverse proxy (nginx, Caddy) that handles:

  • TLS termination
  • Request size limits
  • Connection timeouts
  • Static file serving

Run Node.js as a non-root user:

FROM node:20-slim
RUN groupadd -r app && useradd -r -g app app
WORKDIR /app
COPY --chown=app:app . .
USER app
CMD ["node", "server.js"]

Dependency Security

Express applications live in the npm ecosystem. Apply standard npm security practices:

npm ci
npm audit --audit-level=high
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

How Safeguard.sh Helps

Safeguard.sh monitors the full dependency tree of your Express applications. It tracks every npm package version across your services, correlates against vulnerability databases in real time, and provides a consolidated dashboard for your Node.js supply chain risk. When a critical CVE hits Express or any middleware package, Safeguard.sh identifies every affected service in your organization and delivers prioritized remediation steps to your team.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.