Overview
Broken Authentication vulnerabilities occur when session handling, tokens, or credentials are not properly protected, enabling impersonation or unauthorized access. In the real world, attackers can hijack sessions, reset passwords, or access restricted data if tokens are leaked via cookies, logs, or URLs. This guide covers how these issues appear in Node.js (Express) apps and how to mitigate them. No CVEs are provided here; this is a general, technically accurate remediation guide for common patterns in this framework.
In Express-based apps, common manifestations include storing JWTs or session IDs in non-httpOnly cookies, trusting client-side state for access control, weak or missing token verification, and insecure password storage. Additional risks include long-lived tokens, predictable token secrets, and password reset flows that can be abused. These patterns enable attackers to bypass login or maintain access after initial compromise.
Remediation focuses on secure session management and strong authentication controls. Best practices include using httpOnly and Secure cookies with SameSite, validating tokens on every request, employing short-lived access tokens with rotation, and hashing passwords with bcrypt. Consider MFA, CSRF protection for cookie-based auth, rate-limiting login attempts, auditing, and rotating secrets through environment variables.
Implementing these controls together reduces the risk of Broken Authentication and helps detect anomalous behavior quickly. Regular security testing, dependency updates, and careful logging are essential for sustaining secure authentication in Node.js (Express).
Code Fix Example
Node.js (Express) API Security Remediation
/* Vulnerable pattern (Express app) */
const express = require('express');
const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.json());
app.use(cookieParser());
const JWT_SECRET = 'supersecret';
const users = [{ id: 1, username: 'alice', password: 'password123' }];
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (!user) return res.status(401).send('Invalid credentials');
const token = jwt.sign({ sub: user.id }, JWT_SECRET, { expiresIn: '1h' });
// Vulnerable: cookie without httpOnly/secure flags
res.cookie('token', token);
res.json({ ok: true });
});
function auth(req, res, next) {
const token = req.cookies?.token;
if (!token) return res.status(401).send('Not authenticated');
try {
jwt.verify(token, JWT_SECRET);
next();
} catch (e) {
res.status(401).send('Not authenticated');
}
}
app.get('/dashboard', auth, (req, res) => {
res.json({ data: 'secret' });
});
app.listen(3000, () => console.log('Vulnerable app listening on 3000'));
/* Fixed pattern (Express app) */
const express2 = require('express');
const jwt2 = require('jsonwebtoken');
const cookieParser2 = require('cookie-parser');
const bcrypt = require('bcrypt');
const app2 = express2();
app2.use(express2.json());
app2.use(cookieParser2());
const JWT_SECRET2 = process.env.JWT_SECRET || 'supersecret';
const users2 = [{ id: 1, username: 'alice', passwordHash: bcrypt.hashSync('password123', 10) }];
app2.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users2.find(u => u.username === username);
if (!user) return res.status(401).send('Invalid credentials');
if (!bcrypt.compareSync(password, user.passwordHash)) return res.status(401).send('Invalid credentials');
const token = jwt2.sign({ sub: user.id }, JWT_SECRET2, { expiresIn: '15m' });
// Fixed: store token in httpOnly, secure cookie with SameSite and short expiry
res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'Strict', maxAge: 15 * 60 * 1000 });
res.json({ ok: true });
});
function authFixed(req, res, next) {
const token = req.cookies?.token;
if (!token) return res.status(401).send('Not authenticated');
jwt2.verify(token, JWT_SECRET2, (err, payload) => {
if (err) return res.status(401).send('Not authenticated');
req.user = payload;
next();
});
}
app2.get('/dashboard', authFixed, (req, res) => {
res.json({ data: 'secret', userId: req.user?.sub });
});
app2.listen(3001, () => console.log('Fixed app listening on 3001'));