Обработчик входа сохраняет до-логинную сессию — найдите и исправьте изъян.
Ниже обработчики входа и выхода веб-приложения на Express.
Условия:
- проверка пароля уже корректна, менять её не нужно;
- хранилище сессий поддерживает
regenerate()иdestroy(); - приложение работает только по HTTPS.
app.post('/login', async (req, res) => {
const user = await findUser(req.body.email);
if (!user || !(await verify(req.body.password, user.hash))) {
return res.status(401).json({ error: 'invalid credentials' });
}
req.session.userId = user.id;
res.cookie('sid', req.sessionID);
res.json({ ok: true });
});
app.post('/logout', (req, res) => {
req.session.userId = null;
res.json({ ok: true });
});
Найдите и исправьте уязвимость управления сессией.
Обработчик оставляет идентификатор, с которым пришёл посетитель, поэтому подсунувший его владеет сессией — классическая фиксация, а выход лишь обнуляет поле. Перевыдавайте сессию при входе и смене прав, уничтожайте при выходе, укрепите cookie.
- ✗Считать, что хранилище сессий само меняет идентификатор при входе
- ✗Считать обнулённое поле пользователя при выходе уничтоженной сессией
- ✗Отдавать сессионную cookie без
HttpOnly,SecureиSameSite
- →Какое ещё событие, кроме входа, обязано вызывать перевыдачу сессии?
- →Почему выход обязан удалять серверную запись, а не только cookie?
Уязвимость — фиксация сессии. Приложение оставляет тот же идентификатор, с которым посетитель пришёл на страницу входа, поэтому злоумышленник, заранее подсунувший жертве известное ему значение sid, после её успешного входа получает полностью аутентифицированную сессию. Второй дефект — выход: он обнуляет поле userId, но серверная запись сессии остаётся живой, так что ранее украденный идентификатор продолжает работать.
Исправление: перевыдать сессию сразу после проверки пароля, уничтожать её при выходе и выставить защитные атрибуты cookie.
app.post('/login', async (req, res) => {
const user = await findUser(req.body.email);
if (!user || !(await verify(req.body.password, user.hash))) {
return res.status(401).json({ error: 'invalid credentials' });
}
req.session.regenerate((err) => {
if (err) return res.status(500).end();
req.session.userId = user.id;
req.session.createdAt = Date.now();
res.cookie('sid', req.sessionID, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
});
res.json({ ok: true });
});
});
app.post('/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('sid');
res.json({ ok: true });
});
});
Тот же вызов regenerate() нужен при любой смене привилегий — повышении роли, смене пароля, прохождении дополнительного фактора, — иначе старый идентификатор наследует новые права.