Skip to content

Security

Express apps die from a thousand cuts. Missing header here , permissive CORS there , no rate limiting , no CSRF token , SQL query string concatenation - each one is a CVE waiting to happen
Express gives you the rope. These packages help you not hang yourself with it. But they're only effective if you configure them correctly. Default configs protect against the lazy attacker. Customize for your threat model

Helmet.js - security headers

Helmet sets HTTP security headers that block entire classes of attacks

npm install helmet
const helmet = require('helmet')
app.use(helmet())  // sets 15+ security headers with safe defaults

What Helmet sets:

  • Content-Security-Policy - prevents XSS and data injection
  • X-Content-Type-Options - prevents MIME type sniffing
  • Strict-Transport-Security - enforces HTTPS
  • X-Frame-Options - prevents clickjacking
  • X-XSS-Protection - legacy XSS filter (mostly deprecated now)
  • Referrer-Policy - controls referrer header
  • Permissions-Policy - restricts browser features
  • Removes X-Powered-By - hides Express

Customize for your app:

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'" , "'unsafe-inline'" , 'cdn.example.com'],
      styleSrc: ["'self'" , "'unsafe-inline'"],
      imgSrc: ["'self'" , 'data:' , 'images.example.com'],
      connectSrc: ["'self'" , 'api.example.com'],
      fontSrc: ["'self'" , 'fonts.googleapis.com'],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: []
    }
  },
  hsts: {
    maxAge: 63072000,      // 2 years
    includeSubDomains: true,
    preload: true
  }
}))

CORS configuration

Cross-Origin Resource Sharing - controls which origins can read your responses

npm install cors
const cors = require('cors')

// WIDE OPEN - for development only
app.use(cors())

// RESTRICTED - for production
const corsOptions = {
  origin: [
    'https://yourapp.com',
    'https://admin.yourapp.com'
  ],
  methods: ['GET' , 'POST' , 'PUT' , 'DELETE'],
  allowedHeaders: ['Content-Type' , 'Authorization'],
  credentials: true,          // allow cookies/auth headers
  maxAge: 86400               // cache preflight for 24 hours
}
app.use(cors(corsOptions))

// PER ROUTE
app.get('/api/public' , cors() , handler)
app.post('/api/private' , cors(corsOptions) , handler)

// NEVER do this:
// app.use(cors({ origin: true }))   // reflects origin - dangerous
// app.use(cors({ origin: '*' }))    // allows every origin

rate limiting

Prevent brute force , DoS , and scraping

npm install express-rate-limit
const rateLimit = require('express-rate-limit')

// global rate limit
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,    // 15 minutes
  max: 100,                     // 100 requests per window
  standardHeaders: true,        // Return rate limit info in headers
  legacyHeaders: false,         // Disable X-RateLimit-* headers
  message: { error: 'Too many requests , try again later' }
})
app.use(globalLimiter)

// strict rate limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,                       // 5 attempts per 15 minutes
  message: { error: 'Too many login attempts' }
})
app.use('/login' , authLimiter)
app.use('/register' , authLimiter)

SQL injection prevention

Parameterized queries. Every time. No exceptions

// DANGEROUS - string concatenation
const query = `SELECT * FROM users WHERE id = ${req.params.id}`  // SQLi heaven

// SAFE - parameterized query with pg (PostgreSQL)
const { Pool } = require('pg')
const pool = new Pool({ connectionString: process.env.DATABASE_URL })

app.get('/users/:id' , async (req , res) => {
  const result = await pool.query(
    'SELECT * FROM users WHERE id = $1',
    [req.params.id]
  )
  res.json(result.rows)
})

// SAFE - with mysql2
const mysql = require('mysql2/promise')
const connection = await mysql.createConnection(process.env.DATABASE_URL)
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE id = ?',
  [req.params.id]
)

XSS prevention

Reflected and stored XSS are the most common web vulnerabilities

// 1. Helmet's CSP blocks inline scripts
app.use(helmet.contentSecurityPolicy({
  directives: {
    scriptSrc: ["'self'"]  // no inline scripts , no eval
  }
}))

// 2. Escape output in templates - always use <%= %> not <%- %>
// In EJS: <%= user.name %>  (escaped)
// In EJS: <%- user.name %>  (raw - XSS if user-controlled)

// 3. Sanitize user input
const createDOMPurify = require('dompurify')
const { JSDOM } = require('jsdom')
const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)

const clean = DOMPurify.sanitize(req.body.htmlContent)

CSRF prevention

Cross-Site Request Forgery - making authenticated users perform actions they didn't intend

npm install csrf-csrf
const { doubleCsrf } = require('csrf-csrf')
const { generateToken , doubleCsrfProtection } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET,
  cookieName: 'csrf-token',
  cookieOptions: {
    httpOnly: true,
    sameSite: 'strict',
    secure: true
  },
  size: 64,
  getTokenFromRequest: (req) => req.headers['x-csrf-token']
})

app.use(doubleCsrfProtection)

// get CSRF token
app.get('/csrf-token' , (req , res) => {
  res.json({ csrfToken: generateToken(req , res) })
})

// protected routes
app.post('/transfer' , (req , res) => {
  // CSRF protection auto-validates
  res.json({ transferred: true })
})

security checklist

// 1. Helmet - security headers
app.use(helmet())

// 2. CORS - restrict origins
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }))

// 3. Rate limiting
app.use(globalLimiter)
app.use('/auth' , authLimiter)

// 4. Body size limits
app.use(express.json({ limit: '10kb' }))
app.use(express.urlencoded({ limit: '10kb' }))

// 5. CSRF protection
app.use(doubleCsrfProtection)

// 6. Hide Express
app.disable('x-powered-by')

// 7. Trust proxy (if behind Nginx/Cloudflare)
app.set('trust proxy', 1)

// 8. HTTP parameter pollution protection
// Use express-validator's body() or a dedicated HPP middleware

prerequisites

express_11_auth.md - Passport strategies , bcrypt , JWT , protected routes


next → express_13_validation.md