Безопасная аутентификация в JavaScript: как не спалить пароли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Пароль "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: атака, о которой все забывают

Представьте: пользователь авторизован на вашем сайте. Он переходит по ссылке на другой сайт, а там —

Блоги

Рекомендаций по книгам