Код-ревью 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.
Что должен найти ревьюер
- Безопасностьстрока 3Секрет JWT захардкожен и слабый: утечёт с репозиторием, перебирается. Брать из окружения/хранилища секретов, длинный случайный.
- Безопасностьстрока 6MD5 без соли для паролей: подбирается на GPU и по радужным таблицам. Нужен argon2id, bcrypt или scrypt с солью.
- Безопасностьстрока 7
...req.bodyв insert: mass assignment, пользователь сам задастrole: "admin". Только явные поля после валидации. - Безопасностьстрока 13Разные ответы «нет пользователя» и «неверный пароль» позволяют перечислять зарегистрированные email. Нужен один общий ответ 401.
- Безопасностьстрока 15Нет ограничения попыток входа: перебор паролей. Нужен rate limit по IP и аккаунту.
- Безопасностьстрока 16Токен без
expiresIn: бессрочный, его нельзя отозвать. Короткий access + refresh с ротацией. - Безопасностьстрока 20
Math.randomне криптостойкий: токен сброса предсказуем. Нуженcrypto.randomBytes(32), хранить хеш токена и срок жизни. - Безопасностьстрока 26
jwt.decodeне проверяет подпись: любой подделает токен с любым id и ролью. Нуженjwt.verifyс явным алгоритмом. - Ошибкастрока 26Заголовок
Authorizationсодержит префиксBearer, его надо отрезать; при отсутствии заголовка должен быть 401.
Разбор
Почти полный набор классических ошибок аутентификации. На собесе стоит пройтись по слоям:
- Хранение паролей: медленный хеш с солью (argon2id или bcrypt), не MD5/SHA-256.
- Вход: одинаковый ответ на «нет пользователя» и «неверный пароль», rate limit, сравнение секретов за постоянное время там, где сравниваете сами (
crypto.timingSafeEqual). - Токены: секрет из окружения,
verifyа неdecode, явный список алгоритмов, короткий срок жизни, проверкаaud/iss. - Сброс пароля: криптостойкий одноразовый токен с коротким сроком, хранится хеш, после использования все сессии пользователя отзываются.
- Mass assignment: в БД только явно разрешённые поля.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.
Предыдущий вопрос
Кто превысил лимит запросов: скользящее окно
Следующий вопрос
Как защитить Node.js API