This Express API handles user authentication. Itβs in production. It has 12 security vulnerabilities. How many can you spot?
The code
const express = require('express');
const mysql = require('mysql');
const [jwt](/blog/jwt-decoder/) = require('jsonwebtoken');
const app = express();
app.use(express.json());
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password123',
database: 'myapp'
});
const JWT_SECRET = 'mysecretkey123';
app.post('/login', (req, res) => {
const { username, password } = req.body;
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
db.query(query, (err, results) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (results.length > 0) {
const token = jwt.sign(
{ id: results[0].id, role: results[0].role },
JWT_SECRET
);
res.json({ token, user: results[0] });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
});
app.get('/users/:id', (req, res) => {
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
db.query(query, (err, results) => {
if (err) return res.status(500).json({ error: err.message });
res.json(results[0]);
});
});
app.delete('/users/:id', (req, res) => {
db.query(`DELETE FROM users WHERE id = ${req.params.id}`, (err) => {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: 'User deleted' });
});
});
app.listen(3000, () => console.log('Server running'));
Find the vulnerabilities, then scroll down.
The 12 vulnerabilities
1. π SQL Injection (login)
// β String interpolation in SQL = game over
const query = `SELECT * FROM users WHERE username = '${username}'`;
// Input: ' OR '1'='1' --
// Becomes: SELECT * FROM users WHERE username = '' OR '1'='1' --'
// β
Parameterized queries
const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.query(query, [username, password], callback);
This is the #1 web vulnerability. An attacker can dump your entire database.
2. π SQL Injection (get user)
Same problem on the GET endpoint. req.params.id goes straight into SQL.
3. π SQL Injection (delete user)
And again on DELETE. Every endpoint is injectable.
4. π Passwords stored in plain text
The query compares password = '${password}' β meaning passwords are stored as plain text in the database.
// β
Hash passwords with bcrypt
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12);
// Compare:
const match = await bcrypt.compare(inputPassword, storedHash);
5. π Hardcoded database credentials
// β Credentials in source code
password: 'password123'
// β
[Environment variables](/blog/what-is-environment-variables/)
password: process.env.DB_PASSWORD
6. π Hardcoded JWT secret
// β Weak, hardcoded secret
const JWT_SECRET = 'mysecretkey123';
// β
Strong, from environment
const JWT_SECRET = process.env.JWT_SECRET; // 256-bit random string
7. β° JWT has no expiration
// β Token valid forever
jwt.sign({ id, role }, secret);
// β
Add expiration
jwt.sign({ id, role }, secret, { expiresIn: '1h' });
8. π€ Returns entire user object
// β Sends password hash, internal fields, everything
res.json({ token, user: results[0] });
// β
Select specific fields
res.json({ token, user: { id: results[0].id, username: results[0].username } });
9. π« No authentication on GET/DELETE
Anyone can read or delete any user. No token verification.
// β
Add auth middleware
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
req.user = jwt.verify(token, JWT_SECRET);
next();
} catch { res.status(401).json({ error: 'Invalid token' }); }
}
app.get('/users/:id', authenticate, handler);
10. π« No authorization check
Even with auth, any logged-in user could delete any other user. Need role/ownership checks.
11. π Error messages leak internals
// β Sends raw database errors to client
res.status(500).json({ error: err.message });
// β
Generic error to client, detailed log server-side
console.error(err);
res.status(500).json({ error: 'Internal server error' });
12. π‘οΈ No rate limiting
No protection against brute-force login attempts. An attacker can try millions of passwords.
// β
Add rate limiting
const rateLimit = require('express-rate-limit');
app.use('/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 10 }));
Bonus issues (not security, but still bad)
- No input validation (username/password could be undefined)
- No CORS configuration
- No helmet middleware for security headers
- Using
mysqlpackage instead ofmysql2(no promise support) - Database connection not pooled
The scary part
This code pattern is in thousands of tutorials online. If you learned Express from a YouTube tutorial, check your code against this list.
Related: AI Security Checklist