Смена пароля — пропущенный шаг re-auth
Типичная реализация "смены пароля" от джуна:
app.post('/change-password', auth, async (req, res) => {
const { newPassword } = req.body;
await db.query(
'UPDATE users SET password = ? WHERE id = ?',
[hash(newPassword), req.user.id]
);
res.json({ message: 'Password changed successfully' });
});
На ревью всё выглядит правильно:
- есть middleware
auth— пользователь аутентифицирован; - пароль хешируется перед записью в БД;
- SQL через плейсхолдеры — injection закрыт;
- обновляется только
req.user.id— чужой аккаунт не тронут.
Критический шаг всё же пропущен — сервер не проверяет текущий пароль (и не требует второй фактор). Достаточно действующей сессии или токена, чтобы навсегда сменить пароль и выбить владельца из аккаунта.
Хеши, соль и атаки на пароли — устройство и надёжность паролей. Сессии, JWT и отзыв токенов — аутентификация и авторизация.
Что отсутствует
Re-authentication — повторное подтверждение личности перед чувствительной операцией.
Для смены пароля клиент должен прислать минимум:
currentPassword(илиoldPassword) — строка, которую знает только владелец;newPassword— новое значение.
Сервер сравнивает currentPassword с хешем в БД (bcrypt.compare, argon2.verify). Только после успеха — UPDATE.
| Проверка | Есть в коде джуна | Зачем |
|---|---|---|
Пользователь залогинен (auth) | Да | Базовый барьер |
| Новый пароль хешируется | Да | Защита at rest |
| Текущий пароль верен | Нет | Доказательство, что это владелец, а не украденная сессия |
| CSRF-токен (cookie-auth) | Нет | Защита от подделки запроса из браузера |
| Инвалидация других сессий | Нет | Старый украденный токен остаётся валидным |
Middleware auth отвечает на вопрос "кто делает запрос по токену?", но не на "владелец ли аккаунта прямо сейчас?". Украденная сессия для этого эндпоинта равносильна знанию пароля.
Атака — захват аккаунта через сессию
Сценарий session hijacking (перехват сессии):
- Жертва залогинена в сервисе.
- Атакующий получает session cookie или JWT — XSS на том же origin, вредонсное расширение, malware, утечка токена в лог, HTTP без TLS на публичном Wi‑Fi.
- Атакующий шлёт
POST /change-passwordс своимnewPassword. Знание старого пароля не нужно. - Пароль в БД меняется. Жертва не может войти со старым паролем.
- Атакующий входит с новым паролем (или продолжает пользоваться украденным токеном, если сессии не отозваны).
Это полный захват аккаунта (account takeover): не "прочитал профиль", а закрепил контроль сменой секрета.
См. также JWT — семь строк, которые обходят авторизацию — компрометированный bearer-токен даёт тот же эффект на stateless API.
Вторая дыра — CSRF при cookie-сессии
Если auth держится на cookie (sessionId, HttpOnly), браузер сам прикладывает cookie к POST /change-password с любого сайта, пока жертва залогинена.
Вредоносная страница:
<form action="https://bank.example/change-password" method="POST">
<input type="hidden" name="newPassword" value="pwned123!" />
</form>
<script>document.forms[0].submit();</script>
Жертва открыла вкладку — форма ушла с её cookie. Сервер видит валидную сессию и меняет пароль без текущего пароля и без CSRF-токена.
Митигация CSRF — в статье о безопасности приложений — SameSite, anti-CSRF token, для API — не полагаться только на cookie без дополнительных мер.
Проверка текущего пароля CSRF не заменяет (атакующий может подставить и его, если знает — но при hijack сценарии пароль как раз неизвестен). Нужны оба слоя там, где применимо.
Безопасная реализация
Минимальный каркас:
app.post('/change-password', auth, csrf, async (req, res) => {
const { currentPassword, newPassword } = req.body;
const user = await db.query('SELECT password_hash FROM users WHERE id = ?', [
req.user.id,
]);
const ok = await verifyPassword(currentPassword, user.password_hash);
if (!ok) {
return res.status(403).json({ error: 'Invalid current password' });
}
if (!isStrongEnough(newPassword)) {
return res.status(400).json({ error: 'Password too weak' });
}
await db.query('UPDATE users SET password = ? WHERE id = ?', [
hash(newPassword),
req.user.id,
]);
await revokeAllSessionsExcept(req.user.id, req.session.id);
res.json({ message: 'Password changed' });
});
Дополнительно (production):
- Письмо "пароль изменён" — пользователь заметит взлом;
- Rate limit на эндпоинт — против перебора
currentPassword; - 2FA / WebAuthn для смены пароля в чувствительных продуктах;
- Единый стиль ошибок — не раскрывать, существует ли email на
/reset-password(отдельная тема).
У аккаунтов "Войти через Google" локального пароля может не быть — тогда re-auth это redirect на IdP (prompt=login) или проверка свежести сессии (auth_time, max_age в OIDC).
Чек-лист
- Запрос содержит
currentPassword; сервер сверяет с хешем в БД. - При cookie-auth — CSRF-токен или
SameSite=Strict/Lax+ не GET для смены. - После смены — отзыв остальных сессий и refresh-токенов.
- Уведомление по email/SMS.
- Rate limiting на
/change-passwordи/login. - Новый пароль проходит политику сложности и не совпадает с текущим.
- Логирование события без записи паролей в plain text.
Связанные материалы
| Тема | Статья |
|---|---|
| Хеширование и атаки на пароли | Устройство и надёжность паролей |
| Отзыв JWT и "выйти везде" | Аутентификация и авторизация |
| CSRF | 8.07 / 113 — CSRF |
| Каталог угроз API | 8.07 / 128 |
| Регресс "смена пароля" | 7.05 / 128 — тест-кейсы |