Код-ревью Middle Безопасность бэкенда спрашивают часто обновлено

Код-ревью: регистрация, вход и сброс пароля

Вопрос

Код аутентификации небольшого API. Найди уязвимости и ошибки.

const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const SECRET = 'supersecret';

app.post('/register', async (req, res) => {
  const hash = crypto.createHash('md5').update(req.body.password).digest('hex');
  await db.users.insert({ ...req.body, password: hash });
  res.json({ ok: true });
});

app.post('/login', async (req, res) => {
  const user = await db.users.findOne({ email: req.body.email });
  if (!user) return res.status(404).json({ error: 'User not found' });
  const hash = crypto.createHash('md5').update(req.body.password).digest('hex');
  if (hash !== user.password) return res.status(401).json({ error: 'Wrong password' });
  res.json({ token: jwt.sign({ id: user.id, role: user.role }, SECRET) });
});

app.post('/reset', async (req, res) => {
  const token = Math.random().toString(36).slice(2);
  await db.resets.insert({ email: req.body.email, token });
  res.json({ sent: true });
});

app.get('/me', (req, res) => {
  const payload = jwt.decode(req.headers.authorization);
  res.json(payload);
});

Коротко

Почти полный набор классических ошибок аутентификации. На собесе стоит пройтись по слоям: 1.

Что должен найти ревьюер

  1. Безопасностьстрока 3
    Секрет JWT захардкожен и слабый: утечёт с репозиторием, перебирается. Брать из окружения/хранилища секретов, длинный случайный.
  2. Безопасностьстрока 6
    MD5 без соли для паролей: подбирается на GPU и по радужным таблицам. Нужен argon2id, bcrypt или scrypt с солью.
  3. Безопасностьстрока 7
    ...req.body в insert: mass assignment, пользователь сам задаст role: "admin". Только явные поля после валидации.
  4. Безопасностьстрока 13
    Разные ответы «нет пользователя» и «неверный пароль» позволяют перечислять зарегистрированные email. Нужен один общий ответ 401.
  5. Безопасностьстрока 15
    Нет ограничения попыток входа: перебор паролей. Нужен rate limit по IP и аккаунту.
  6. Безопасностьстрока 16
    Токен без expiresIn: бессрочный, его нельзя отозвать. Короткий access + refresh с ротацией.
  7. Безопасностьстрока 20
    Math.random не криптостойкий: токен сброса предсказуем. Нужен crypto.randomBytes(32), хранить хеш токена и срок жизни.
  8. Безопасностьстрока 26
    jwt.decode не проверяет подпись: любой подделает токен с любым id и ролью. Нужен jwt.verify с явным алгоритмом.
  9. Ошибкастрока 26
    Заголовок Authorization содержит префикс Bearer , его надо отрезать; при отсутствии заголовка должен быть 401.

Разбор

Почти полный набор классических ошибок аутентификации. На собесе стоит пройтись по слоям:

  1. Хранение паролей: медленный хеш с солью (argon2id или bcrypt), не MD5/SHA-256.
  2. Вход: одинаковый ответ на «нет пользователя» и «неверный пароль», rate limit, сравнение секретов за постоянное время там, где сравниваете сами (crypto.timingSafeEqual).
  3. Токены: секрет из окружения, verify а не decode, явный список алгоритмов, короткий срок жизни, проверка aud/iss.
  4. Сброс пароля: криптостойкий одноразовый токен с коротким сроком, хранится хеш, после использования все сессии пользователя отзываются.
  5. Mass assignment: в БД только явно разрешённые поля.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.

Термины по теме