Пароль "qwerty123" больше не работает: почему безопасность — это не скучно
Представьте: вы написали крутое веб-приложение. Красивый интерфейс, реактивный код, всё летает. Вы запускаете его, регистрируете первого пользователя — и через час ваша база данных утекает в открытый доступ. Знакомая история? К сожалению, безопасная аутентификация в JavaScript — это та тема, которую многие разработчики откладывают на потом. А зря.
В этой статье мы разберём, как защитить ваше приложение от самых распространённых атак: перехвата токенов, XSS, CSRF и утечки паролей. Мы не будем читать лекции про «надо делать хорошо» — мы покажем код, который можно использовать прямо сейчас.
Готовы? Поехали.
Аутентификация в JavaScript: почему клиент — это поле боя
Когда мы говорим об аутентификации в вебе, мы часто думаем о бэкенде: базы данных, сессии, хеши. Но JavaScript аутентификация начинается на клиенте. И здесь нас подстерегают ловушки.
Всё, что работает в браузере, может быть прочитано, изменено или украдено. Ваш JavaScript-код — это открытая книга для злоумышленника, если он нашёл уязвимость. Поэтому первое правило безопасной аутентификации: никогда не доверяйте клиенту. Никогда.
Основные угрозы на клиентской стороне
- XSS (Cross-Site Scripting) — злоумышленник внедряет скрипт, который крадёт токены из localStorage.
- CSRF (Cross-Site Request Forgery) — пользователь авторизован, а злонамеренный сайт отправляет запросы от его имени.
- Man-in-the-Middle — перехват трафика, если не используется HTTPS.
- Утечка паролей — отправка пароля в открытом виде или хранение его в коде.
Звучит страшно? На самом деле, защититься от этого не так сложно. Главное — знать, какие инструменты использовать.
JWT: удобно, но с подвохом
JWT (JSON Web Token) — это стандарт для создания токенов доступа. Он лёгкий, не требует хранения на сервере и отлично подходит для SPA. Но популярность делает его мишенью.
Многие разработчики хранят JWT в localStorage. Это ошибка. localStorage доступен из любого JavaScript-кода на странице. Если у вас есть хотя бы одна XSS-уязвимость — токен украден.
Как хранить JWT правильно
Лучший способ — httpOnly cookie. Куки с флагом httpOnly недоступны для JavaScript. Их не украдёт XSS. Но есть нюанс: куки автоматически отправляются с каждым запросом, что делает приложение уязвимым к CSRF. Решение — использовать SameSite и CSRF токены.
Вот пример установки httpOnly cookie на бэкенде (Node.js с Express):
// Сервер (Node.js / Express)
res.cookie('token', jwtToken, {
httpOnly: true, // Недоступен для JavaScript
secure: true, // Только по HTTPS
sameSite: 'strict', // Защита от CSRF
maxAge: 3600000 // 1 час
});
На клиенте вы просто отправляете запросы — куки приложатся автоматически:
// Клиент (JavaScript)
fetch('/api/profile', {
method: 'GET',
credentials: 'include' // Обязательно!
})
.then(res => res.json())
.then(data => console.log(data));
Обратите внимание: credentials: 'include' — без этого куки не отправятся на другой origin.
Хеширование паролей: bcrypt вам в помощь
Хранить пароли в открытом виде — это как оставить ключи от дома под ковриком. Даже если ваш сайт взломают, пароли должны быть бесполезны для злоумышленника.
bcrypt — это алгоритм хеширования, который специально сделан медленным. Потому что хешировать пароль для пользователя — это доли секунды, а для подбора — годы. В JavaScript (Node.js) используйте библиотеку bcrypt.
Пример регистрации и входа с bcrypt
// Установка: npm install bcrypt
const bcrypt = require('bcrypt');
const saltRounds = 12; // Чем выше, тем безопаснее, но медленнее
// Регистрация
async function registerUser(password) {
const hash = await bcrypt.hash(password, saltRounds);
// Сохраняем hash в БД
return hash;
}
// Вход
async function loginUser(password, hashFromDB) {
const match = await bcrypt.compare(password, hashFromDB);
if (match) {
console.log('Пароль верный!');
} else {
console.log('Неверный пароль');
}
}
Важно: никогда не используйте MD5 или SHA для паролей. Они быстрые, а значит — легко подбираются. Только bcrypt, scrypt или Argon2.
Защита от XSS: санитайзинг и CSP
XSS — это когда злоумышленник внедряет скрипт на вашу страницу. Например, через поле комментария. Если вы выводите данные без экранирования, скрипт выполнится.
Вот как НЕ надо делать:
// Опасно! Никогда так не делайте
document.getElementById('output').innerHTML = userInput;
А вот как надо:
// Безопасно
document.getElementById('output').textContent = userInput;
textContent не интерпретирует HTML. Но если вам нужно вывести HTML (например, форматированный текст), используйте библиотеку-санитайзер, например DOMPurify:
// Установка: npm install dompurify
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(userInput);
document.getElementById('output').innerHTML = cleanHTML;
Content Security Policy (CSP)
CSP — это HTTP-заголовок, который запрещает выполнение скриптов из непроверенных источников. Добавьте его на сервере:
// Сервер (Express)
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' https://trusted-cdn.com"
);
next();
});
CSP не даст выполниться даже встроенным скриптам (<script>alert('xss')</script>), если вы не разрешите 'unsafe-inline'. А вы не разрешите, правда?
CSRF: атака, о которой все забывают
Представьте: пользователь авторизован на вашем сайте. Он переходит по ссылке на другой сайт, а там —