Overview
Broken Authentication in Node.js (Express) can allow attackers to impersonate legitimate users, access protected resources, or escalate privileges if session handling, token validation, or password storage are weak. In real-world deployments, misconfigured cookies, plaintext password storage, or insecure JWT handling can enable credential stuffing, session hijacking, or unauthorized data access. When credentials or session tokens are predictable, not rotated, or not bound to a secure transport, attackers can reuse them across endpoints, bypass login, or maintain persistent access even after logout. This guide provides a general remediation approach for these patterns in Express-based applications.
In this guide, no specific CVE IDs are provided. The described issues reflect common Broken Authentication patterns observed in Node.js/Express stacks, including insecure cookie attributes (HttpOnly, Secure, SameSite), plaintext password verification, and improper token validation. The focus is on measurable hardening techniques, such as server-side sessions or properly signed tokens, password hashing, and strict verification on protected routes.
When authentication state is stored or transmitted via cookies or tokens, every point of entry-login, session creation, and access to protected resources-must enforce strict integrity, confidentiality, and expiration checks. Minimal exposure of error information and consistent security configurations across environments help reduce the blast radius of any credential-related breach. Follow these remediation steps and implement the accompanying code patterns to reduce Broken Authentication risk in Express apps.
Code Fix Example
Node.js (Express) API Security Remediation
// Vulnerable pattern
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const app = express();
app.use(bodyParser.json());
app.use(cookieParser());
// Insecure user store (plaintext) and simple token in a cookie (not HttpOnly/Secure)
const usersV = { alice: 'password123' };
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (usersV[username] && usersV[username] === password) {
// Vulnerable: token stored in a client-accessible cookie; no server-side session
const token = 'token-' + Date.now();
res.cookie('session', token, { httpOnly: false, secure: false, sameSite: 'lax' });
return res.json({ ok: true });
}
res.status(401).json({ ok: false });
});
function isAuthV(req) {
const t = req.cookies && req.cookies.session;
return !!t;
}
app.get('/protected', (req, res) => {
if (isAuthV(req)) {
return res.json({ secret: '42' });
}
res.status(401).json({ error: 'Unauthorized' });
});
// Secure pattern (improves on vulnerability)
const bcrypt = require('bcrypt');
const session = require('express-session');
const users = {};
(async function init() {
// Pre-hash a sample user for demonstration
const hash = await bcrypt.hash('password123', 12);
users['alice'] = hash;
})();
app.use(session({
name: 'sessionId',
secret: 'CHANGE_ME',
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 }
}));
app.post('/login-secure', async (req, res) => {
const { username, password } = req.body;
const hash = users[username];
if (!hash) return res.status(401).json({ ok: false });
const ok = await bcrypt.compare(password, hash);
if (ok) {
req.session.user = username;
return res.json({ ok: true });
}
res.status(401).json({ ok: false });
});
app.get('/protected-secure', (req, res) => {
if (req.session && req.session.user) {
return res.json({ secret: '42' });
}
res.status(401).json({ error: 'Unauthorized' });
});
app.listen(3000, () => console.log('Server listening on port 3000'));