πŸ“š Learning Hub
Β· 3 min read

Code Review This: An Express API That Will Get Hacked


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 mysql package instead of mysql2 (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

πŸ“˜