Безопасность JavaScript приложений: топ-10 уязвимостей на практике

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

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

Начать курс

Введение: почему безопасность JavaScript приложений — это не опция, а необходимость

JavaScript (особенно на платформе Node.js) стал доминирующим языком для создания как клиентских, так и серверных приложений. Однако с ростом популярности растет и количество атак. По данным OWASP, большинство взломов происходят из-за типовых ошибок, которые можно предотвратить. В этой статье мы разберем топ-10 уязвимостей JavaScript (Node.js) с реальными примерами кода и методами защиты. Материал будет полезен как начинающим, так и опытным разработчикам, желающим укрепить безопасность своих проектов.

1. Инъекции (SQL, NoSQL, Command Injection)

Инъекции остаются самой опасной категорией уязвимостей. В Node.js часто страдают MongoDB и PostgreSQL.

Пример уязвимого кода (NoSQL-инъекция):

// Уязвимый код
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = await db.collection('users').findOne({
    username: username,
    password: password
  });
  // Если передать { "username": { "$ne": null }, "password": { "$ne": null } }, то вернется первый пользователь
});

Как исправить:

  • Используйте ORM/ODM с параметризацией (Mongoose, Sequelize).
  • Валидируйте и санируйте входные данные (библиотека validator или joi).
  • Для команд используйте execFile вместо exec.

2. Межсайтовый скриптинг (XSS)

XSS позволяет злоумышленнику внедрить вредоносный скрипт в страницу, которую просматривают другие пользователи.

Пример уязвимого кода (Reflected XSS):

// Уязвимый Express-маршрут
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h1>Результаты поиска для: ${query}</h1>`);
  // Если передать ?q=<script>alert('XSS')</script>, скрипт выполнится
});

Защита:

  • Экранируйте вывод: используйте res.render с шаблонизаторами (EJS, Pug) или encodeURIComponent.
  • Установите HTTP-заголовок Content-Security-Policy.
  • Не используйте innerHTML на клиенте — применяйте textContent.

3. Небезопасная аутентификация и управление сессиями

Слабая аутентификация — одна из главных причин утечек данных. Часто разработчики хардкодят ключи или используют слабые пароли.

Типичные ошибки:

  • Хранение паролей в открытом виде (вместо bcrypt/argon2).
  • Использование предсказуемых JWT-секретов (secret123).
  • Отсутствие ограничения на количество попыток входа.

Пример правильной реализации:

const bcrypt = require('bcrypt');
const saltRounds = 12;

app.post('/register', async (req, res) => {
  const hash = await bcrypt.hash(req.body.password, saltRounds);
  // Сохраняем hash в БД
});

app.post('/login', async (req, res) => {
  const user = await db.getUser(req.body.username);
  const match = await bcrypt.compare(req.body.password, user.passwordHash);
  if (match) {
    // Создаем JWT с коротким сроком действия
    const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '15m' });
    res.json({ token });
  }
});

4. SSRF (Server-Side Request Forgery)

Уязвимость SSRF позволяет атакующему заставить сервер выполнять запросы к внутренним ресурсам (например, к метаданным облачного провайдера).

Пример:

// Уязвимый код: загрузка аватара по URL
app.post('/avatar', async (req, res) => {
  const { url } = req.body;
  const response = await fetch(url); // Можно передать http://169.254.169.254/latest/meta-data/
  // Обработка ответа
});

Защита:

  • Создайте белый список разрешенных доменов.
  • Блокируйте запросы к внутренним IP-адресам (127.0.0.1, 10.x.x.x, 172.16.x.x, 192.168.x.x).
  • Используйте библиотеку ssrf-req-filter.

5. Небезопасная десериализация

Node.js уязвим к атакам через eval, JSON.parse с опасными данными или использование node-serialize.

Опасный пример:

const serialize = require('node-serialize');
app.post('/state', (req, res) => {
  const obj = serialize.unserialize(req.body.data);
  // Если передать сконструированный объект с __proto__, можно изменить прототип
});

Решение:

  • Используйте только безопасные форматы (JSON без функций).
  • Никогда не применяйте eval() или new Function() для пользовательских данных.
  • Для сложных объектов используйте библиотеки с проверкой типов (например, class-validator).

6. Недостатки логирования и мониторинга

Отсутствие логирования критических событий (неудачные попытки входа, изменение прав) делает невозможным обнаружение атаки.

Что логировать:

  • Все попытки аутентификации (успешные и неуспешные).
  • Изменения ролей и прав доступа.
  • Доступ к конфиденциальным данным.
  • Ошибки валидации и исключения.

Лучшие практики:

  • Используйте структурированное логирование (Winston, Pino).
  • Никогда не логи

Блоги

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