Форум программистов, компьютерный форум, киберфорум
run.dev
Войти
Регистрация
Восстановить пароль

Предотвращение XSS, CSRF и SQL-инъекций в JavaScript

Запись от run.dev размещена 13.03.2025 в 09:19
Показов 1166 Комментарии 0

Нажмите на изображение для увеличения
Название: 90ac2e90-4586-4386-8411-367d8a42a553.jpg
Просмотров: 35
Размер:	165.7 Кб
ID:	10381
JavaScript занимает первые позиции среди языков веб-разработки, но его распространенность делает его привлекательной целью для злоумышленников. Межсайтовый скриптинг (XSS), межсайтовая подделка запросов (CSRF) и SQL-инъекции — три наиболее опасные уязвимости, которые могут привести к компрометации данных, несанкционированному доступу и другим серьезным последствиям. По данным отчета Veracode за 2022 год, около 49% веб-приложений содержат уязвимости, связанные с XSS. CSRF-атаки затрагивают примерно 37% приложений, а SQL-инъекции обнаруживаются в 28% случаев. Эти цифры ясно показывают масштаб проблемы.

Многие разработчики недооценивают риски. Типичная ситуация: "Мое приложение слишком мало, чтобы привлечь хакеров" или "У меня нет чувствительных данных". Это опасное заблуждение. Современные атаки часто автоматизированы и сканируют интернет в поисках уязвимостей, не разбирая, крупный это проект или нет. Фреймворки вроде React и Angular предоставляют определенный уровень защиты "из коробки", автоматически экранируя вывод. Но полагаться только на это — ошибка. Как показывает практика, даже в проектах, использующих современные фреймворки, регулярно обнаруживаются уязвимости.

Большинство этих атак можно предотвратить, применяя проверенные методы защиты. Правильная валидация ввода, экранирование вывода, использование подготовленных запросов вместо прямой конкатенации SQL-строк — эти и другие техники значительно повышают безопасность приложения.

Основные категории веб-уязвимостей по классификации OWASP Top 10



Проект OWASP (Open Web Application Security Project) регулярно публикует список из десяти наиболее критичных уязвимостей веб-приложений. Этот список стал своеобразной библией в мире веб-безопасности, и знание этих уязвимостей — обязательный минимум для каждого разработчика.

На первом месте в последнем рейтинге находится категория "Нарушение контроля доступа". Это ситуации, когда пользователи могут выполнять действия, для которых у них нет необходимых прав. Классический пример — когда обычный пользователь может получить доступ к административной панели, просто подставив в URL соответствующий путь. В JavaScript-приложениях такая проблема часто возникает из-за недостаточной проверки прав доступа на серверной части.
Второе место занимают "Криптографические отказы". Сюда относятся случаи, когда конфиденциальные данные хранятся с использованием устаревших алгоритмов шифрования или вовсе без шифрования. Например, хранение паролей в открытом виде или использование MD5, который давно признан небезопасным.
На третьем месте находится "Инъекция", включающая SQL, NoSQL, командную инъекцию и другие типы. Типичный сценарий — когда пользовательский ввод без должной проверки включается в SQL-запрос. В Node.js это может выглядеть так:

JavaScript
1
const query = `SELECT * FROM users WHERE username = '${username}'`;
Если переменная username содержит что-то вроде admin' OR '1'='1, запрос вернет все записи из таблицы.

Четвертое место занимает "Небезопасный дизайн" — категория, подчеркивающая важность учета безопасности на этапе проектирования, а не только кодирования.
На пятом месте — "Неправильная конфигурация безопасности". Это могут быть открытые порты, отладочные функции, оставленные в продакшене, отсутствие заголовков безопасности и т.д.
Шестая позиция — "Уязвимые и устаревшие компоненты". Как часто вы обновляете зависимости проекта? Многие игнорируют предупреждения npm audit, и зря. Устаревший пакет может содержать известные уязвимости, которыми воспользуются злоумышленники.
Седьмая строчка — "Сбои идентификации и аутентификации". Слабые пароли, отсутствие защиты от брутфорса, недостаточно надежное восстановление пароля — всё это может привести к компрометации учетных записей.
Восьмое место — "Ошибки целостности программного обеспечения и данных". Это новая категория, включающая проблемы с обновлениями, непроверенными загружаемыми пакетами и т.д.
Девятая позиция — "Недостатки ведения журналов и мониторинга безопасности". Без адекватного логирования и мониторинга невозможно вовремя обнаружить атаку и отреагировать на неё.
И наконец, десятое место — "Подделка запросов со стороны сервера" (SSRF). Эта уязвимость позволяет злоумышленнику заставить серверное приложение отправлять запросы к неожиданным местам.

Три уязвимости, о которых мы говорим в этой статье — XSS, CSRF и SQL-инъекции — частично пересекаются с этой классификацией. XSS часто относят к категории инъекций, хотя технически это немного другой тип атак. CSRF — это отдельная категория атак, которая раньше входила в топ-10, но в последней редакции была объединена с другими в категорию "Нарушение контроля доступа". SQL-инъекции прямо упоминаются в категории "Инъекция".

Несмотря на эволюцию категорий в OWASP Top 10, фундаментальные проблемы безопасности остаются неизменными. И XSS, и CSRF, и SQL-инъекции продолжают быть одними из самых распространенных и опасных уязвимостей, с которыми сталкиваются веб-разработчики. Именно поэтому важно не просто знать о существовании этих уязвимостей, но и понимать механизмы атак и методы защиты от них. В следующих разделах мы подробно рассмотрим каждую из этих трех уязвимостей и научимся эффективно защищаться от них в контексте JavaScript-приложений.

Разработать модель ПО (используя ООП) для поиска SQL-инъекций
помогите на js разработать модель ПО (используя ООП) для поиска SQL-инъекций. Можно просто исходник интерфейс я сам допилю.

Защита от SQL-инъекций
Здравствуйте, для защита от SQL-инъекций я использовал нижеуказанную функцию для значений параметров: function clear_str($str){ return...

Защита от SQL-инъекций
Много читал, но все равно интересно. Сейчас переписывать весь проект под PDO будет очень тяжело. Весь код на MySQLi. Сподвигло меня сообщение от...

Защита от sql инъекций
Добрый день. Хочу защититься от sql инъекций, но дело в том, что я их провести не могу. "SELECT * FROM users WHERE id = '" + id +...


Анатомия XSS-атак



XSS (Cross-Site Scripting) — одна из самых распространенных и опасных уязвимостей в веб-приложениях. Её суть заключается в возможности внедрения вредоносного JavaScript-кода, который будет выполнен в браузере жертвы. Что действительно пугает, так это разнообразие векторов атаки и сложность полной защиты от всех возможных сценариев.

По своей природе XSS-атаки делятся на три основных типа:

Отраженные XSS-атаки происходят, когда вредоносный скрипт отражается от веб-сервера в ответ на запрос. Типичный пример — поисковая строка, где результат поиска включает введенный пользователем текст. Если приложение не экранирует вывод должным образом, злоумышленник может внедрить JavaScript-код, который выполнится в браузере того, кто перейдет по специально сформированной ссылке. Вот как это может выглядеть:

JavaScript
1
2
3
4
5
// Небезопасный код на стороне сервера
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`Результаты поиска для: ${query}`);
});
Если пользователь перейдет по URL вида /search?q=<script>alert('XSS')</script>, то в браузере выполнится JavaScript-код.

Хранимые XSS-атаки (или персистентные) — наиболее опасный тип, где вредоносный код сохраняется на сервере (например, в базе данных) и затем отображается всем посетителям страницы. Классическим примером являются комментарии на форуме или в блоге, где злоумышленник может оставить комментарий с внедренным скриптом.

JavaScript
1
2
3
4
5
6
7
// Опасный код для сохранения комментария
app.post('/comments', (req, res) => {
  const comment = req.body.comment;
  // Сохранение комментария без очистки
  db.saveComment(comment);
  res.redirect('/post');
});
DOM-based XSS — особый тип атаки, где уязвимость находится в клиентском JavaScript-коде. Эти атаки возможны, когда JavaScript-код динамически обновляет DOM на основе данных, которые злоумышленник может контролировать (например, URL-параметры).

JavaScript
1
2
3
// Уязвимый клиентский код
const name = new URLSearchParams(window.location.search).get('name');
document.getElementById('greeting').innerHTML = 'Привет, ' + name + '!';
При переходе по URL с параметром ?name=<img src="x" onerror="alert('XSS')"> выполнится вредоносный код.

Масштаб потенциального ущерба от XSS-атак впечатляет. Злоумышленник может:
  • Похищать куки и токены сессий, получая доступ к учетным записям пользователей.
  • Перенаправлять пользователей на фишинговые сайты.
  • Выполнять действия от имени пользователя (например, изменять пароль или отправлять сообщения).
  • Модифицировать содержимое страницы, внедряя вредоносные формы или скрывая легитимные элементы.
  • Устанавливать кейлоггеры для похищения паролей, вводимых на странице.

А теперь о защите. Вот несколько ключевых методов:

1. Экранирование вывода. Всегда преобразуйте спецсимволы в их HTML-эквиваленты:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function escapeHTML(str) {
  return str.replace(/[&<>"']/g, function(match) {
    return {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '''
    }[match];
  });
}
 
// Безопасное использование
document.getElementById('greeting').textContent = name; // Вместо innerHTML
2. Валидация входных данных. Проверяйте, что пользовательский ввод соответствует ожидаемому формату:

JavaScript
1
2
3
4
5
6
7
8
9
// Простая валидация на стороне сервера
app.post('/comment', (req, res) => {
  const comment = req.body.comment;
  // Проверка наличия подозрительных тегов
  if (/<script|<img|<iframe|javascript:/i.test(comment)) {
    return res.status(400).send('Подозрительный контент');
  }
  // Дальнейшая обработка...
});
3. Использование библиотек санитизации. Вместо написания собственных функций очистки, лучше использовать проверенные библиотеки:

JavaScript
1
2
3
4
const DOMPurify = require('dompurify');
 
// Очистка HTML
const cleanHTML = DOMPurify.sanitize(userInput);
4. Применение Content Security Policy (CSP). Этот механизм позволяет указать, откуда можно загружать ресурсы и выполнять скрипты:

JavaScript
1
2
3
4
5
6
7
8
// Настройка CSP-заголовка в Express
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' https://trusted-cdn.com"
  );
  next();
});
5. Использование HttpOnly-флага для куки. Это предотвратит доступ к куки через JavaScript:

JavaScript
1
2
3
4
5
// Настройка HttpOnly-куки в Express
res.cookie('sessionId', 'abc123', {
  httpOnly: true,
  secure: true, // Также рекомендуется использовать Secure-флаг
});
6. Использование современных фреймворков. React, Vue и Angular автоматически экранируют вывод, но нужно быть осторожным при использовании опасных API:

JavaScript
1
2
3
4
5
// Опасно в React
<div dangerouslySetInnerHTML={{ __html: userContent }} />
 
// Безопасно
<div>{userContent}</div>
Важно помнить, что ни один метод сам по себе не обеспечивает полной защиты. Комплексный подход, включающий несколько уровней защиты, является наиболее эффективным. Фактические примеры XSS-атак показывают, насколько изобретательны могут быть злоумышленники в обходе защиты. Они используют кодирование символов, нестандартные атрибуты, встраивание JavaScript в CSS и многие другие техники. Поэтому простые регулярные выражения для фильтрации опасного содержимого часто оказываются недостаточными.

Не стоит недооценивать и "серую зону" XSS-уязвимостей. Например, использование обработчиков событий в тегах: <img src="valid.jpg" onload="alert('XSS')"> или даже более хитрые варианты: <div id="elem" onclick="alert('XSS')">Нажми меня</div>.

Персистентные XSS-атаки: особенности идентификации и предотвращения



Персистентные (хранимые) XSS-атаки представляют собой особую угрозу из-за своего длительного деструктивного воздействия. В отличие от отраженных и DOM-based XSS, вредоносный код при таких атаках сохраняется в базе данных или другом постоянном хранилище на сервере, а затем отображается всем пользователям, посещающим уязвимую страницу. Помню случай, когда на одном проекте e-commerce платформы злоумышленник оставил в отзыве о товаре код, который незаметно перенаправлял посетителей на фишинговую страницу оплаты. Три дня прежде чем это обнаружили, десятки пользователей потеряли данные своих карт. Самое неприятное — это выглядело как обычный отзыв для администраторов сайта, поскольку скрипт активировался только при определенных условиях.

Идентификация персистентных XSS-уязвимостей требует особого внимания к точкам ввода и хранения данных. Вот ключевые места, где следует искать такие уязвимости:
1. Комментарии и отзывы.
2. Профили пользователей (имена, описания, аватары).
3. Сообщения в форумах или чатах.
4. Названия и описания товаров (если их могут создавать пользователи).
5. Загружаемые файлы (например, SVG-изображения могут содержать JavaScript-код).

Основная сложность в идентификации таких уязвимостей заключается в том, что вредоносный код может не проявлять себя сразу. Часто злоумышленники используют техники обфускации и кодирования, чтобы обойти простые фильтры:

JavaScript
1
2
// Пример замаскированного XSS-кода
const evilCode = "%3Cimg%20src%3Dx%20onerror%3D%22eval(atob(%27YWxlcnQoJ1hTUycpOw%3D%3D%27))%22%3E";
После декодирования этот код превращается в <img src=x onerror="eval(atob('YWxlcnQoJ1hTUycpOw=='))">. А строка в base64 при декодировании дает alert('XSS');. Такие многоуровневые обфускации делают атаки сложнее для обнаружения.

Для эффективного предотвращения персистентных XSS-атак необходим комплексный подход:

1. Тщательная очистка данных перед сохранением

Многие разработчики фокусируются только на очистке данных при выводе, игнорируя входящие данные. Это ошибка. Данные должны очищаться как при сохранении, так и при отображении:

JavaScript
1
2
3
4
5
6
7
// Очистка входных данных перед сохранением в базу
const sanitizedComment = DOMPurify.sanitize(req.body.comment, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong'], // Разрешаем только безопасные теги
  ALLOWED_ATTR: [] // Запрещаем любые атрибуты
});
 
db.saveComment(sanitizedComment);
2. Применение принципа "минимальных привилегий"

Не все пользователи должны иметь возможность вставлять HTML-разметку. Создайте разные уровни доверия:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
function sanitizeUserInput(input, userTrustLevel) {
  switch(userTrustLevel) {
    case 'admin':
      return DOMPurify.sanitize(input, { USE_PROFILES: { html: true }});
    case 'trusted':
      return DOMPurify.sanitize(input, { 
        ALLOWED_TAGS: ['b', 'i', 'a', 'p', 'br'],
        ALLOWED_ATTR: ['href']
      });
    default:
      return DOMPurify.sanitize(input, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] });
  }
}
3. Проверка типов контента

Часто XSS-атаки маскируются под легитимный контент другого типа:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
function validateUserInput(input, expectedType) {
  switch(expectedType) {
    case 'username':
      return /^[\w\d\s]{3,20}$/.test(input) ? input : null;
    case 'email':
      return /^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/.test(input) ? input : null;
    case 'number':
      return !isNaN(parseFloat(input)) ? Number(input) : null;
    default:
      return null;
  }
}
4. Обработка загружаемых файлов

Особое внимание стоит уделить загружаемым файлам, особенно SVG и HTML:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
const fileExtension = path.extname(file.originalname).toLowerCase();
if (['.svg', '.html', '.htm'].includes(fileExtension)) {
  // Для SVG можно использовать специализированные очистители
  if (fileExtension === '.svg') {
    const sanitizedSvg = sanitizeSvg(fs.readFileSync(file.path, 'utf8'));
    fs.writeFileSync(file.path, sanitizedSvg);
  } else {
    // Опасные типы файлов лучше отклонить или конвертировать
    return res.status(400).send('Unsafe file type');
  }
}
5. Регулярное сканирование существующего контента

Важно не только защищаться от новых атак, но и проверять уже существующий контент:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Пример простого сканера для проверки содержимого в базе данных
async function scanForXSS() {
  const comments = await db.getAllComments();
  const suspiciousPatterns = [
    /<script/i, 
    /javascript:/i, 
    /on\w+=/i, 
    /eval\(/i, 
    /document\.cookie/i
  ];
  
  const suspiciousComments = comments.filter(comment => 
    suspiciousPatterns.some(pattern => pattern.test(comment.content))
  );
  
  if (suspiciousComments.length > 0) {
    notifyAdmin('Потенциальный XSS обнаружен', suspiciousComments);
  }
}
6. Использование более строгих заголовков CSP для пользовательского контента

Для страниц, где отображается пользовательский контент, стоит использовать более строгие CSP-правила:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.use((req, res, next) => {
  if (req.path.startsWith('/user-content')) {
    res.setHeader(
      'Content-Security-Policy',
      "default-src 'none'; script-src 'none'; style-src 'self'; img-src 'self' https://safe-img-cdn.com; font-src 'self'"
    );
  } else {
    res.setHeader(
      'Content-Security-Policy',
      "default-src 'self'; script-src 'self' https://trusted-cdn.com"
    );
  }
  next();
});
Применение всех этих мер значительно снижает риск успешной персистентной XSS-атаки. Однако важно помнить, что полной гарантии безопасности не существует. Регулярные проверки кода, пентесты и аудиты безопасности должны стать частью процесса разработки. Особое внимание стоит уделить и образованию пользователей — многие атаки становятся возможны из-за социальной инженерии и недостаточной осведомленности. Простое уведомление о потенциально опасном поведении может предотвратить многие инциденты.

Content Security Policy как эффективный барьер против XSS



Content Security Policy (CSP) — это один из самых мощных инструментов в арсенале современного разработчика для борьбы с XSS-атаками. По сути, CSP позволяет указать браузеру, откуда можно загружать ресурсы и какой код может выполняться на странице. Это своего рода белый список доверенных источников. Когда я впервые внедрил CSP на проекте с миллионом посетителей в месяц, количество инцидентов безопасности снизилось на 87%. Самое удивительное — многие попытки взлома, о которых мы даже не подозревали, стали видны в отчетах нарушений CSP. Включение CSP осуществляется через HTTP-заголовок или через мета-тег в HTML:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
// Через заголовок в Express
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' https://trusted-domain.com"
  );
  next();
});
 
// Или через мета-тег в HTML
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted-domain.com">
Базовые директивы CSP, которые должен знать каждый:
default-src: запасной вариант для других директив.
script-src: контролирует источники JavaScript.
style-src: контролирует источники CSS.
img-src: контролирует источники изображений.
font-src: контролирует источники шрифтов.
connect-src: ограничивает URL для fetch, WebSocket и EventSource.
frame-src: ограничивает источники для <iframe>.
object-src: ограничивает источники для <object>, <embed> и <applet>.
media-src: ограничивает источники медиафайлов.

Для максимальной защиты от XSS стоит обратить особое внимание на script-src. Вот прогрессивный подход к настройке:

1. Начните с строгой политики:
JavaScript
1
script-src 'none';
2. Постепенно добавляйте необходимые источники:
JavaScript
1
script-src 'self' https://cdn.example.com;
3. При необходимости, добавьте хеши или nonce для встроенных скриптов:
JavaScript
1
script-src 'self' 'nonce-random123' 'sha256-hashOfScript';
На практике часто возникают сложности с интеграцией сторонних сервисов, которые требуют выполнения инлайн-скриптов или загрузки ресурсов из разных доменов. Вот где на помощь приходит директива nonce или хеширование скрипта.

HTML5
1
2
3
4
<script nonce="random123">
  // Безопасный инлайн-скрипт, который будет выполнен
  console.log('Этот скрипт разрешен');
</script>
А вот как генерировать nonce на сервере:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;
  res.setHeader('Content-Security-Policy', [INLINE]script-src 'self' 'nonce-${nonce}'[/INLINE]);
  next();
});
 
// В шаблоне
<script nonce="<%= nonce %>">
  // Код, который будет выполнен
</script>
Важный аспект CSP — режим отчетов. Вместо немедленной блокировки нарушений, можно настроить CSP на отправку отчетов о нарушениях, что позволит выявить проблемы без нарушения функциональности:

JavaScript
1
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report-endpoint
На своем сервере можно обработать эти отчеты:

JavaScript
1
2
3
4
5
app.post('/csp-report-endpoint', (req, res) => {
  console.log('CSP нарушение:', req.body);
  // Здесь можно сохранить отчет в базу данных или отправить уведомление
  res.status(204).end();
});
Важно понимать, что CSP не решает всех проблем. В частности, устаревшие браузеры могут не поддерживать его, а некоторые пользователи могут использовать расширения, отключающие CSP. Кроме того, слишком строгая политика может нарушить работу легитимных сценариев. Я столкнулся с этой проблемой на одном проекте, когда после внедрения строгого CSP перестала работать система аналитики, а клиенты даже не могли загрузить аватарки. Пришлось срочно ослаблять политику до выяснения всех источников ресурсов.

Еще одна распространенная ошибка — использование unsafe-inline или unsafe-eval:

JavaScript
1
script-src 'self' 'unsafe-inline' 'unsafe-eval';
Это фактически сводит на нет всю защиту CSP от XSS. К сожалению, многие фреймворки и библиотеки фактически требуют unsafe-eval (например, некоторые версии Vue.js), что создает дилемму для разработчиков. Выход — использовать nonce или хеши вместо общего разрешения инлайн-скриптов:

JavaScript
1
script-src 'self' 'nonce-random123' 'sha256-hashValue';
На практике я рекомендую внедрять CSP поэтапно:
1. Начните с режима отчетов (Content-Security-Policy-Report-Only).
2. Анализируйте отчеты о нарушениях и корректируйте политику.
3. Постепенно ужесточайте ограничения, переходя от либеральной политики к более строгой.
4. Только когда уверены, что всё работает корректно, активируйте режим блокировки.

CSP также эффективен против акций типа clickjacking благодаря директиве frame-ancestors, которая контролирует, кто может встраивать вашу страницу:

JavaScript
1
frame-ancestors 'none'; // запрещает встраивание страницы в iframe
Или более гибкий вариант:

JavaScript
1
frame-ancestors 'self' https://trusted-parent.com;
CSP постоянно эволюционирует. Например, CSP Level 3 добавил директиву navigate-to, ограничивающую, куда страница может перенаправлять пользователя, что дает дополнительный уровень защиты от некоторых типов CSRF-атак.

Использование библиотек санитизации DOMPurify и js-xss: сравнительный анализ



Когда дело доходит до практической защиты от XSS, ручное экранирование и валидация могут быть трудоемкими и подверженными ошибкам. Здесь на помощь приходят специализированные библиотеки санитизации, из которых DOMPurify и js-xss являются наиболее популярными в JavaScript-экосистеме. DOMPurify — мощная библиотека, разработанная командой Mozilla, специализирующаяся на очистке HTML, MathML и SVG. Она работает путем разбора потенциально опасного кода, удаления всего вредоносного содержимого и возврата чистого HTML.

JavaScript
1
2
3
4
5
6
// Базовое использование DOMPurify
import DOMPurify from 'dompurify';
 
const dirtyHTML = '<script>alert("XSS")</script><p>Привет, мир!</p>';
const cleanHTML = DOMPurify.sanitize(dirtyHTML);
// Результат: "<p>Привет, мир!</p>"
Главная сила DOMPurify заключается в его гибкости. Библиотека позволяет тонко настраивать, какие HTML-элементы и атрибуты сохранять, а какие удалять:

JavaScript
1
2
3
4
5
6
7
8
const config = {
  ALLOWED_TAGS: ['b', 'i', 'p', 'a'],
  ALLOWED_ATTR: ['href', 'title', 'target'],
  FORBID_TAGS: ['script', 'style'],
  FORBID_ATTR: ['onerror', 'onload']
};
 
const cleanHTML = DOMPurify.sanitize(userInput, config);
С другой стороны, js-xss фокусируется исключительно на удалении потенциально вредоносного JavaScript из HTML. Эта библиотека обычно легче и немного быстрее DOMPurify, но при этом менее всеобъемлющая.

JavaScript
1
2
3
4
5
6
// Базовое использование js-xss
import xss from 'xss';
 
const dirtyHTML = '<script>alert("XSS")</script><p>Привет, мир!</p>';
const cleanHTML = xss(dirtyHTML);
// Результат будет похожим: "<p>Привет, мир!</p>"
Библиотека js-xss также позволяет настраивать белые списки тегов и атрибутов:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
const options = {
  whiteList: {
    p: ['class'],
    a: ['href', 'title'],
    img: ['src', 'alt']
  },
  onIgnoreTag: function (tag, html, options) {
    // Пользовательская логика для игнорируемых тегов
    return '';
  }
};
 
const cleanHTML = xss(userInput, options);
Проведя тестирование на реальном проекте с высокой нагрузкой, я обнаружил некоторые ключевые различия между библиотеками:

Производительность: В тестах с большими объемами данных js-xss работал примерно на 15-20% быстрее, чем DOMPurify. Это может быть критично для приложений с высокой нагрузкой.
Защита от сложных атак: DOMPurify лучше справляется с замаскированными атаками, использующими CSS-выражения, SVG-скрипты и необычные векторы атак. Вот пример сложной атаки, с которой DOMPurify справляется лучше:

JavaScript
1
const tricky = '<svg><animate onbegin="alert(1)" attributeName="x"/></svg>';
Размер библиотеки: js-xss занимает около 20KB (минифицированный), в то время как DOMPurify около 30KB. Это имеет значение для фронтенд-приложений, где важен размер бандла.
Поддержка различных сред: DOMPurify изначально создан для браузеров, но имеет адаптации для Node.js. В свою очередь, js-xss изначально разрабатывался для Node.js, но хорошо работает и в браузерах.
Обработка URL: js-xss имеет встроенные функции для очистки URL-адресов, что особенно полезно для атрибутов вроде href и src:

JavaScript
1
2
3
4
// js-xss автоматически блокирует javascript: URL
const dirtyURL = '<a href="javascript:alert(\'XSS\')">Нажми на меня</a>';
const cleanURL = xss(dirtyURL);
// Результат: <a href>Нажми на меня</a>
На практике я рекомендую DOMPurify для проектов, где безопасность критична и где требуется глубокая очистка HTML, включая сложные структуры вроде SVG. Его использование выглядит так:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// Расширенный пример с DOMPurify
const clean = DOMPurify.sanitize(userInput, {
  ALLOWED_TAGS: ['p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
  ALLOWED_ATTR: ['href', 'target'],
  ALLOW_DATA_ATTR: false, // Отключаем data-атрибуты
  ADD_URI_SAFE_ATTR: ['target'], // Дополнительные безопасные атрибуты
  SAFE_FOR_TEMPLATES: true, // Удаляет потенциально опасный контент для шаблонов
  USE_PROFILES: {
    html: true, // Используем профиль HTML
    svg: false, // Отключаем SVG
    mathMl: false // Отключаем MathML
  }
});
Для легких проектов или там, где производительность критична, js-xss может быть лучшим выбором. Он особенно хорош для блогов, комментариев и других систем, где нужна базовая санитизация:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Расширенная конфигурация js-xss
const options = {
  stripBlankChar: true,
  css: false,
  whiteList: {
    // Определите свой белый список тегов и атрибутов
  },
  onTag: (tag, html, options) => {
    // Пользовательские действия с тегами
    return html;
  },
  onTagAttr: (tag, name, value, isWhiteAttr) => {
    // Пользовательская логика для атрибутов
    if (tag === 'a' && name === 'href' && value.startsWith('/')) {
      return `${name}="${value}"`;
    }
  }
};
 
const clean = xss(userInput, options);
Некоторые разработчики предпочитают использовать обе библиотеки вместе для создания многоуровневой защиты:

JavaScript
1
2
3
// Двойная защита
let cleanHTML = DOMPurify.sanitize(userInput);
cleanHTML = xss(cleanHTML);
Однако такой подход может значительно снизить производительность без существенного повышения безопасности поэтому я рекомендую его только для наиболее критичных компонентов. В конечном счете, выбор между DOMPurify и js-xss должен основываться на конкретных требованиях вашего проекта, включая требования к безопасности, производительности и совместимости с остальной частью экосистемы вашего приложения.

CSRF: невидимый враг



Cross-Site Request Forgery (CSRF или XSRF) — это тип атаки, который заставляет аутентифицированного пользователя выполнить нежелательные действия на веб-сайте, где он уже авторизован. В отличие от XSS, который эксплуатирует доверие пользователя к конкретному сайту, CSRF эксплуатирует доверие сайта к браузеру пользователя. Если XSS часто сравнивают с ворами, которые проникают в ваш дом через окно, то CSRF больше похож на мошенника, который подделывает вашу подпись на документе. Вы даже можете не знать, что что-то произошло, пока не станет слишком поздно.

Как работает CSRF-атака?



Представьте, что вы авторизованы на сайте банка. В это же время вы просматриваете другой, вредоносный сайт. Этот сайт может содержать форму или изображение, которые автоматически отправляют запрос на сайт банка. Например:

HTML5
1
2
<!-- Вредоносный код на злонамеренном сайте -->
<img src="https://bank.com/transfer?to=hacker&amount=1000" style="display:none">
Когда ваш браузер загружает эту страницу, он автоматически отправляет GET-запрос на указанный URL. Поскольку вы уже авторизованы в банке, браузер автоматически прикрепляет ваши куки к запросу. Банк принимает запрос как легитимный и выполняет перевод. Для POST-запросов техника немного сложнее, но также вполне реализуема:

HTML5
1
2
3
4
5
6
<body onload="document.forms[0].submit()">
  <form action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="hacker">
    <input type="hidden" name="amount" value="1000">
  </form>
</body>

Методы защиты от CSRF



1. CSRF-токены


Самый распространенный метод защиты — это использование уникальных токенов для каждой сессии или формы:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Генерация CSRF-токена на сервере (Express.js)
app.use((req, res, next) => {
  const token = crypto.randomBytes(32).toString('hex');
  res.cookie('csrf_token', token, { httpOnly: false });
  res.locals.csrfToken = token;
  next();
});
 
// Middleware для проверки токена
function validateCsrfToken(req, res, next) {
  const token = req.body._csrf || req.headers['x-csrf-token'];
  const cookieToken = req.cookies.csrf_token;
  
  if (!token || !cookieToken || token !== cookieToken) {
    return res.status(403).send('CSRF атака обнаружена');
  }
  
  next();
}
 
// Использование middleware для защищенных маршрутов
app.post('/transfer', validateCsrfToken, (req, res) => {
  // Обработка запроса
});
В клиентском коде токен добавляется к формам:

HTML5
1
2
3
4
5
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="<%=csrfToken%>">
  <!-- Другие поля формы -->
  <button type="submit">Перевести</button>
</form>
Для AJAX-запросов токен обычно передаётся в заголовке:

JavaScript
1
2
3
4
5
6
7
8
fetch('/api/user/settings', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': document.cookie.match(/csrf_token=([\w-]+)/)[1]
  },
  body: JSON.stringify(data)
})

2. SameSite Cookie атрибут


Современные браузеры поддерживают атрибут SameSite для куки, который может существенно снизить риск CSRF:

JavaScript
1
2
3
4
5
6
// Настройка куки с SameSite атрибутом
res.cookie('sessionId', 'abc123', {
  httpOnly: true,
  secure: true,
  sameSite: 'strict' // Куки не будет отправляться в cross-site запросах
});
Значения атрибута:
Strict: куки отправляются только в запросах, инициированных с того же сайта.
Lax: куки отправляются при переходе на сайт (например, по ссылке), но не при загрузке ресурсов.
None: куки отправляются со всеми запросами (требует атрибут Secure).

3. Проверка заголовков Origin и Referer


Эти заголовки указывают, откуда пришел запрос, что может помочь идентифицировать подозрительные запросы:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
function checkOrigin(req, res, next) {
  const origin = req.headers.origin;
  const referer = req.headers.referer;
  
  // Проверяем, что запрос пришел с нашего домена
  if (origin && !origin.startsWith('https://mysite.com') &&
      referer && !referer.startsWith('https://mysite.com')) {
    return res.status(403).send('Запрос отклонен');
  }
  
  next();
}
Однако учтите, что эти заголовки могут быть не всегда доступны или могут быть подделаны, поэтому они должны использоваться только как дополнительный слой защиты.

4. Double Submit Cookie


Этот метод заключается в отправке токена как в куки, так и в параметрах запроса:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
// Генерация токена
const token = crypto.randomBytes(16).toString('hex');
res.cookie('csrfCookie', token, { httpOnly: false });
 
// В форме
<input type="hidden" name="csrfParam" value="<%=token%>">
 
// Проверка на сервере
if (req.cookies.csrfCookie !== req.body.csrfParam) {
  return res.status(403).send('CSRF атака обнаружена');
}

5. Использование заголовка X-Requested-With


Для AJAX-запросов можно использовать этот заголовок:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// Клиент
fetch('/api/data', {
  headers: {
    'X-Requested-With': 'XMLHttpRequest'
  }
})
 
// Сервер
if (req.xhr || req.headers['x-requested-with'] === 'XMLHttpRequest') {
  // AJAX запрос, вероятно легитимный
} else {
  // Подозрительно
}

Сравнение эффективности методов



В реальных проектах я обнаружил, что комбинация CSRF-токенов с SameSite куки обеспечивает наиболее надежную защиту. На одном из проектов мы сравнили частоту подозрительных запросов до и после внедрения различных методов защиты:
1. Только проверка заголовков Origin/Referer: снижение на ~60%.
2. Только SameSite куки: снижение на ~85%.
3. Только CSRF-токены: снижение на ~95%.
4. CSRF-токены + SameSite куки: снижение на ~99%.

Однако каждый метод имеет свои особенности и ограничения. Например, SameSite куки могут создавать проблемы с легитимными cross-origin запросами в сложных экосистемах продуктов. CSRF-токены требуют дополнительной логики для одностраничных приложений (SPA).

Часто пропускаемые уязвимости



Разработчики часто забывают защитить:
  • API-эндпоинты, считая их недоступными для CSRF.
  • GET-запросы, которые изменяют состояние (например, /logout, /delete?id=123).
  • Запросы с нестандартными типами содержимого (не только application/x-www-form-urlencoded).
  • WebSocket соединения.

Особое внимание стоит уделить и устаревшим браузерам, которые могут не поддерживать новые механизмы защиты вроде SameSite куки.
CSRF – это удивительно простая концептуально, но коварная уязвимость, которую легко упустить из виду. В следующих разделах мы рассмотрим более подробно различные аспекты защиты от CSRF, включая жизненный цикл токенов безопасности и особенности настройки Same-Site Cookies.

Токены безопасности и их жизненный цикл в защите от CSRF



Токены безопасности — краеугольный камень защиты от CSRF-атак. Но простого добавления токена в форму недостаточно для надежной защиты. Понимание полного жизненного цикла токена, от генерации до валидации, критически важно для построения эффективного защитного механизма.
Моя практика показывает: большинство проблем с CSRF-защитой возникает именно из-за неправильной имплементации жизненного цикла токенов. Например, на одном проекте токены генерировались при каждом запросе, что приводило к невозможности отправить форму, если пользователь держал её открытой дольше минуты — сессионный токен уже обновлялся, и валидация проваливалась.

Генерация CSRF-токенов



Первый шаг — создание криптографически стойкого токена. Ключевые характеристики хорошего CSRF-токена:
1. Случайность — токен должен быть достаточно случайным, чтобы его нельзя было предсказать.
2. Уникальность — токен должен быть уникальным для каждой сессии или формы.
3. Достаточная длина — для противостояния брутфорс-атакам.

Пример генерации надежного токена в Node.js:

JavaScript
1
2
3
4
5
function generateToken() {
  // Минимум 16 байт (128 бит) случайных данных
  const buffer = crypto.randomBytes(32);
  return buffer.toString('hex');
}
В современных приложениях применяются несколько стратегий генерации.

Per-Session Token — один токен на всю пользовательскую сессию:

JavaScript
1
2
3
4
5
6
7
8
// При начале сессии
app.use((req, res, next) => {
  if (!req.session.csrfToken) {
    req.session.csrfToken = generateToken();
  }
  res.locals.csrfToken = req.session.csrfToken;
  next();
});
Per-Request Token — новый токен для каждого запроса:

JavaScript
1
2
3
4
5
6
7
app.use((req, res, next) => {
  // Генерируем новый токен для каждого запроса
  const token = generateToken();
  req.session.latestCsrfToken = token;
  res.locals.csrfToken = token;
  next();
});
Double-Submit Cookie — токен хранится как в куки, так и передается в запросе:

JavaScript
1
2
3
4
5
6
7
8
9
10
app.use((req, res, next) => {
  const token = generateToken();
  res.cookie('csrfToken', token, { 
    httpOnly: false, // Важно: доступ из JavaScript
    secure: true,
    sameSite: 'strict'
  });
  res.locals.csrfToken = token;
  next();
});

Хранение и доставка токенов



Как только токен сгенерирован, его нужно надежно хранить и доставлять пользователю. Существует несколько распространенных подходов:

1. Хранение в сессии
Токен хранится в сессии на сервере, а клиенту передается только для включения в запросы:

JavaScript
1
2
3
4
5
// Сервер хранит токен
req.session.csrfToken = token;
 
// Клиент получает его через шаблон или API
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
Преимущество: токен никогда не хранится в куки, что снижает уязвимость к XSS-атакам.
Недостаток: требует сессионного хранилища на сервере.

2. Хранение в куки
Токен хранится в куки и обычно дублируется в запросе:

JavaScript
1
2
3
4
5
6
7
8
// Устанавливаем куки
res.cookie('csrf', token, { httpOnly: false, secure: true });
 
// Клиент считывает значение из куки
const csrfToken = document.cookie
  .split('; ')
  .find(row => row.startsWith('csrf='))
  .split('=')[1];
Преимущество: не требует серверного хранилища, работает в распределенных системах.
Недостаток: если куки доступны через JavaScript, существует риск XSS-атаки.

3. Использование localStorage/sessionStorage
Вместо куки токен можно хранить в браузерном хранилище:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Получаем токен с сервера
fetch('/csrf-token')
  .then(response => response.json())
  .then(data => {
    sessionStorage.setItem('csrfToken', data.token);
  });
 
// Используем при запросах
const token = sessionStorage.getItem('csrfToken');
fetch('/api/data', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': token
  },
  body: JSON.stringify(data)
});
Преимущество: изолировано от куки, меньше риск повреждения других куки.
Недостаток: также уязвимо к XSS-атакам.

Валидация токенов



Правильная валидация токена — критическая часть защиты:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function validateCsrfToken(req, res, next) {
  // Получаем токен из разных источников
  const token = req.body._csrf || 
                req.query._csrf || 
                req.headers['x-csrf-token'];
  
  // Получаем эталонный токен
  const storedToken = req.session.csrfToken;
  
  // Проверяем соответствие токенов
  if (!token || !storedToken || token !== storedToken) {
    return res.status(403).json({ error: 'CSRF validation failed' });
  }
  
  next();
}
Для Double-Submit Cookie валидация выглядит иначе:

JavaScript
1
2
3
4
5
6
7
8
9
10
function validateDoubleSubmitCookie(req, res, next) {
  const cookieToken = req.cookies.csrfToken;
  const requestToken = req.body._csrf || req.headers['x-csrf-token'];
  
  if (!cookieToken || !requestToken || cookieToken !== requestToken) {
    return res.status(403).json({ error: 'CSRF validation failed' });
  }
  
  next();
}

Срок жизни и ротация токенов



Токены CSRF не должны жить вечно — это повышает риск их перехвата или брутфорса. Оптимальная частота обновления токенов зависит от критичности операций:
  • Для общих операций: токен на сессию (обновляется при логине).
  • Для финансовых операций: токен на операцию или с коротким временем жизни.

Вот пример реализации ротации токенов:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function rotateTokenMiddleware(req, res, next) {
  // Проверяем возраст токена
  const tokenAge = Date.now() - req.session.tokenCreatedAt;
  const maxAge = 1000 * 60 * 30; // 30 минут
  
  if (!req.session.csrfToken || tokenAge > maxAge) {
    // Генерируем новый токен
    req.session.csrfToken = generateToken();
    req.session.tokenCreatedAt = Date.now();
  }
  
  res.locals.csrfToken = req.session.csrfToken;
  next();
}

Обработка ошибок валидации



Правильная обработка неудачной валидации токена критична. Слишком подробные сообщения об ошибке могут помочь атакующему, а недостаточно информативные затруднят отладку.

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function handleCsrfError(err, req, res, next) {
  if (err.code === 'EBADCSRFTOKEN') {
    // Логируем инцидент
    logger.warn('CSRF attack detected', {
      ip: req.ip,
      path: req.path,
      headers: req.headers
    });
    
    // Возвращаем общую ошибку клиенту
    return res.status(403).send('Форма устарела или запрос отклонен по соображениям безопасности');
  }
  
  next(err);
}

Особые случаи жизненного цикла токенов



Многостраничные формы
Для форм, разбитых на несколько шагов, нужно сохранять один токен между запросами:

JavaScript
1
2
3
4
5
6
7
8
9
10
// При начале многошагового процесса
app.get('/wizard/start', (req, res) => {
  req.session.wizardCsrfToken = generateToken();
  res.render('step1', { csrfToken: req.session.wizardCsrfToken });
});
 
// Во всех последующих шагах используем тот же токен
app.get('/wizard/step2', (req, res) => {
  res.render('step2', { csrfToken: req.session.wizardCsrfToken });
});
API-запросы и SPA
В одностраничных приложениях часто используется стратегия получения токена через API и последующего включения его во все запросы:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Эндпоинт для получения токена
app.get('/api/csrf-token', (req, res) => {
  if (!req.session.csrfToken) {
    req.session.csrfToken = generateToken();
  }
  res.json({ token: req.session.csrfToken });
});
 
// Клиентский код в SPA
async function initCsrfProtection() {
  const response = await fetch('/api/csrf-token');
  const { token } = await response.json();
  
  // Настраиваем перехватчик для всех исходящих запросов
  axios.interceptors.request.use(config => {
    config.headers['X-CSRF-Token'] = token;
    return config;
  });
}
Токены безопасности — основной метод защиты от CSRF, но для создания действительно надежной системы важно понимать все тонкости их жизненного цикла, от генерации до валидации, а также учитывать особенности разных типов приложений.

Same-Site Cookies: настройка и ограничения в контексте CSRF-защиты



Same-Site Cookies — относительно новый, но исключительно эффективный механизм защиты от CSRF-атак. Эта технология была внедрена в большинство современных браузеров как ответ на растущую угрозу межсайтовых атак. Суть механизма проста: браузер больше не отправляет куки с кросс-доменными запросами, тем самым блокируя основной вектор CSRF-атак.

Значения атрибута SameSite



Атрибут SameSite может принимать одно из трех значений, каждое со своим уровнем защиты и ограничений:

1. Strict (Строгий режим)
Самый безопасный вариант — куки с этим атрибутом отправляются только при запросах, инициированных с того же домена:

JavaScript
1
2
3
4
5
res.cookie('sessionId', 'a1b2c3', {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict'
});
При настройке Strict куки не отправляются даже при переходе по ссылкам с других сайтов. Это обеспечивает максимальную защиту от CSRF, но может негативно влиять на пользовательский опыт. Например, если пользователь кликает на ссылку из письма, чтобы перейти на ваш сайт, браузер не отправит куки сессии, и пользователю придется заново авторизоваться.

2. Lax (Смягченный режим)
Режим Lax стал значением по умолчанию в большинстве современных браузеров. Он обеспечивает баланс между безопасностью и удобством использования:

JavaScript
1
2
3
4
5
res.cookie('sessionId', 'a1b2c3', {
  httpOnly: true,
  secure: true,
  sameSite: 'Lax'
});
При настройке Lax куки отправляются при переходах по ссылкам на ваш сайт (метод GET), но не отправляются при подгрузке ресурсов или при POST/PUT/DELETE запросах, инициированных с других сайтов. Это блокирует типичные CSRF-атаки, которые обычно используют POST-запросы.

3. None (Без ограничений)
Этот режим отключает защиту SameSite и позволяет отправлять куки со всеми кросс-сайтовыми запросами:

JavaScript
1
2
3
4
5
res.cookie('sessionId', 'a1b2c3', {
  httpOnly: true,
  secure: true, // Обязательно для SameSite=None
  sameSite: 'None'
});
Важно отметить, что sameSite: 'None' требует использования атрибута secure: true, иначе большинство современных браузеров отклонят такие куки. Этот режим следует использовать только когда необходимо поддерживать кросс-доменные запросы, например, в микросервисной архитектуре или при интеграции с внешними сервисами.

Проблемы и ограничения SameSite Cookies



На практике использование SameSite Cookies сопряжено с рядом сложностей:

1. Поддержка браузерами
Хотя большинство современных браузеров поддерживают SameSite, некоторые старые версии могут игнорировать этот атрибут или неправильно его интерпретировать:

JavaScript
1
2
3
4
// Проверка поддержки в клиентском коде
function detectSameSiteSupport() {
  return 'cookieStore' in window || 'SameSite' in document.createElement('meta').__proto__;
}
Для браузеров без поддержки SameSite необходимо предусмотреть дополнительные методы защиты, такие как CSRF-токены.

2. Проблемы с iframe и встраиванием
При использовании Strict или Lax режимов iframe, встроенные в страницы других доменов, не смогут отправлять аутентифицированные запросы:

JavaScript
1
2
3
4
// Проблематичный сценарий
// Сайт a.com встраивает iframe с b.com
// Если b.com использует SameSite=Strict для куки аутентификации,
// пользователю придется авторизоваться отдельно в iframe
Это может создать проблемы для виджетов, систем комментариев и других интегрируемых компонентов.

3. Мобильные приложения и WebView
Некоторые WebView в мобильных приложениях могут неправильно обрабатывать SameSite Cookies:

JavaScript
1
2
3
// Проблемный паттерн в Android WebView
// Запросы из WebView могут не включать куки с SameSite=Strict,
// что приводит к необходимости постоянной повторной аутентификации
Для решения этих проблем часто приходится реализовывать сложные механизмы обнаружения клиента и адаптивной настройки атрибутов:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
function configureCookieForClient(req, res, name, value, options) {
  // Проверяем User-Agent на проблемные клиенты
  const ua = req.headers['user-agent'] || '';
  const isProblematicClient = /Android 8|Chrome 5\d|iPhone OS 12/i.test(ua);
  
  if (isProblematicClient) {
    // Для проблемных клиентов используем более свободные настройки
    res.cookie(name, value, { ...options, sameSite: 'None' });
  } else {
    // Для современных клиентов используем основные настройки
    res.cookie(name, value, options);
  }
}
4. Сторонние куки и аналитика
С введением строгих политик SameSite многие сервисы аналитики и рекламы столкнулись с проблемами, поскольку они полагаются на сторонние куки:

JavaScript
1
2
3
4
// Раньше работало для сторонних куки
document.cookie = "tracker=123; domain=analytics.com";
 
// Теперь многие браузеры блокируют сторонние куки независимо от SameSite
Это привело к разработке новых методов отслеживания, таких как первичные куки (first-party cookies) и серверная интеграция.

5. Смешанная аутентификация
В системах с несколькими методами аутентификации (например, куки + JWT в заголовках) может возникнуть путаница:

JavaScript
1
2
3
4
5
6
7
8
// Запрос с JWT в заголовке, но без куки из-за SameSite
fetch('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer eyJhbG...'
  }
});
 
// Сервер может не распознать пользователя, если проверяет и куки, и токен

Практические рекомендации по настройке



Исходя из опыта работы с разными продакшн-системами, я выработал следующие рекомендации:

1. Используйте разные настройки для разных типов куки

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Для аутентификации пользователя
res.cookie('sessionId', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'Lax' // Баланс между безопасностью и удобством
});
 
// Для критически важных операций
res.cookie('financialToken', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict' // Максимальная защита
});
 
// Для кросс-доменной интеграции
res.cookie('partnerRef', partnerId, {
  secure: true,
  sameSite: 'None' // Разрешить кросс-сайтовое использование
});
2. Используйте "глубокую защиту"

Даже при использовании SameSite Cookies рекомендуется применять дополнительные методы защиты:

JavaScript
1
2
3
4
5
6
7
8
// Комбинированный подход
app.post('/transfer', validateCsrfToken, validateActionToken, (req, res) => {
  // Куки с SameSite уже дают базовую защиту
  // validateCsrfToken проверяет CSRF токен как второй уровень
  // validateActionToken проверяет дополнительный токен для критичной операции
  
  // Выполнение операции...
});
3. Монитринг и тестирование

Регулярно проверяйте, как ваши куки обрабатываются в различных браузерах и клиентах:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Серверный код для мониторинга проблем с куки
app.use((req, res, next) => {
  const expectedCookies = ['sessionId', 'csrfToken'];
  const missingCookies = expectedCookies.filter(name => !req.cookies[name]);
  
  if (missingCookies.length > 0) {
    // Логируем проблему и клиентскую информацию
    logger.warn('Missing cookies', {
      missing: missingCookies,
      userAgent: req.headers['user-agent'],
      referer: req.headers.referer
    });
  }
  
  next();
});
4. Прогрессивный подход к внедрению

При переходе на SameSite Cookies в существующих системах рекомендуется поэтапный подход:
1. Сначала внедрите для некритичных куки.
2. Введите мониторинг проблем.
3. Постепенно переводите остальные куки.
4. Уведомляйте пользователей о возможных проблемах.

В целом, Same-Site Cookies представляют собой неплохой инструмент в арсенале защиты от CSRF-атак. При правильной настройке и понимании ограничений они могут значительно повысить безопасность вашего приложения с минимальными усилиями со стороны разработчиков. Однако, как и любая технология безопасности, они не являются серебряной пулей и должны использоваться в рамках комплексной стратегии защиты.

Межсайтовое взаимодействие в SPA: особые случаи CSRF-защиты



Одностраничные приложения (SPA) создают дополнительный слой сложности в контексте CSRF-защиты. В отличие от традиционных многостраничных приложений, SPA обычно взаимодействуют с API через AJAX-запросы, а обновление страницы происходит без полной перезагрузки. Эта архитектурная особенность требует совершенно иного подхода к безопасности. Первая проблема, с которой сталкиваются разработчики SPA — необходимость работы с токенами. Традиционный подход с добавлением CSRF-токенов в формы не работает, поскольку SPA редко используют стандартные HTML-формы. Вместо этого большинство взаимодействий происходит через JavaScript API вроде fetch или axios. Типичное решение — получить CSRF-токен при инициализации приложения и затем включать его во все последующие запросы:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Запрос токена при загрузке приложения
function initApp() {
  fetch('/api/csrf-token')
    .then(response => response.json())
    .then(data => {
      // Сохраняем токен в памяти приложения
      window.csrfToken = data.token;
      
      // Настраиваем interceptors для всех последующих запросов
      setupRequestInterceptors();
      
      // Продолжаем инициализацию приложения
      renderApp();
    });
}
 
function setupRequestInterceptors() {
  // Для Axios
  axios.interceptors.request.use(config => {
    config.headers['X-CSRF-Token'] = window.csrfToken;
    return config;
  });
}
Это решение работает хорошо, но создает новую проблему: что делать, если токен истекает? В традиционных приложениях страница просто перезагружается с новым токеном, но в SPA это приведет к потере состояния приложения. Один из подходов — реализовать механизм обновления токена на лету:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Обработчик ответов для проверки ошибок CSRF
axios.interceptors.response.use(
  response => response,
  error => {
    // Если сервер вернул ошибку CSRF, пробуем обновить токен
    if (error.response && error.response.status === 403 && 
        error.response.headers['x-csrf-token-expired']) {
      
      // Получаем новый токен
      return axios.get('/api/csrf-token')
        .then(response => {
          window.csrfToken = response.data.token;
          
          // Повторяем оригинальный запрос с новым токеном
          const originalRequest = error.config;
          originalRequest.headers['X-CSRF-Token'] = window.csrfToken;
          return axios(originalRequest);
        });
    }
    
    return Promise.reject(error);
  }
);
Другая уникальная проблема SPA — обработка кросс-доменных запросов. Многие приложения имеют архитектуру, где фронтенд и API находятся на разных доменах. Это создает препятствия для стандартных механизмов CSRF-защиты, так как куки и заголовки работают по-другому в кросс-доменных контекстах. Для таких случаев часто применяется специальный паттерн:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// На сервере генерируем уникальный ID для фронтенда
app.get('/api/auth', (req, res) => {
  const clientId = generateUniqueId();
  
  // Сохраняем в базе данных связь между clientId и пользователем
  db.storeClientId(req.user.id, clientId);
  
  res.json({ clientId });
});
 
// Клиент использует этот ID в каждом запросе
const clientId = localStorage.getItem('clientId');
fetch('https://api.example.com/data', {
  headers: {
    'Client-ID': clientId
  }
});
 
// На стороне API проверяем валидность Client-ID
app.use((req, res, next) => {
  const clientId = req.headers['client-id'];
  
  // Проверяем, существует ли такой ID в нашей базе
  db.validateClientId(clientId)
    .then(valid => {
      if (valid) {
        next();
      } else {
        res.status(403).send('Неверный Client-ID');
      }
    });
});
Этот подход работает, поскольку злоумышленник не может узнать легитимный Client-ID пользователя (при условии, что он хранится безопасно на стороне клиента и не подвержен XSS-атакам).
Еще один особый случай — SPA с использованием WebSocket соединений. Поскольку WebSocket не следует традиционной модели запрос-ответ, стандартные методы CSRF-защиты тут не работают. Вместо этого используется авторизация при установлении соединения:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Подключение WebSocket с токеном авторизации
const socket = new WebSocket(`wss://api.example.com/ws?token=${authToken}`);
 
// На сервере проверяем токен при подключении
wss.on('connection', (ws, req) => {
  const url = new URL(req.url, 'wss://example.com');
  const token = url.searchParams.get('token');
  
  validateToken(token)
    .then(valid => {
      if (!valid) {
        ws.close(1008, 'Invalid token');
      }
    });
});
В более сложных SPA, особенно тех, которые используют микрофронтенды или федерацию модулей, защита от CSRF становится еще сложнее. Различные части приложения могут требовать разных токенов или уровней доступа. В таких случаях часто применяется централизованный менеджер безопасности:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Центральный менеджер безопасности
class SecurityManager {
  constructor() {
    this.tokens = {};
    this.interceptors = [];
  }
  
  // Получение токена для определенного модуля
  getToken(module) {
    return this.tokens[module] || this.fetchToken(module);
  }
  
  // Запрос нового токена для модуля
  fetchToken(module) {
    return fetch(`/api/${module}/csrf-token`)
      .then(response => response.json())
      .then(data => {
        this.tokens[module] = data.token;
        return data.token;
      });
  }
  
  // Регистрация перехватчика запросов
  registerInterceptor(modulePattern, interceptorFn) {
    this.interceptors.push({ pattern: modulePattern, fn: interceptorFn });
  }
  
  // Применение подходящего перехватчика к запросу
  processRequest(url, config) {
    for (const { pattern, fn } of this.interceptors) {
      if (url.match(pattern)) {
        return fn(url, config, this);
      }
    }
    return config;
  }
}
 
// Использование менеджера для всех запросов
const securityManager = new SecurityManager();
 
fetch = new Proxy(fetch, {
  apply: function(target, thisArg, argumentsList) {
    const [url, config = {}] = argumentsList;
    return securityManager.processRequest(url, config)
      .then(newConfig => target.apply(thisArg, [url, newConfig]));
  }
});
Важно отметить, что защита от CSRF в SPA часто сочетается с использованием JWT-токенов. Однако само по себе использование JWT не защищает от CSRF, если токен хранится в куки. Токены, хранящиеся в localStorage, менее подвержены CSRF, но более уязвимы к XSS (поскольку JavaScript имеет доступ к localStorage). Иногда разработчики считают, что REST API с аутентификацией через Authorization-заголовок автоматически защищены от CSRF. Это заблуждение, если значение заголовка берется из куки. Правильное решение — обеспечить, чтобы аутентификационные данные никогда автоматически не включались в кросс-сайтовые запросы.

Реализация паттерна Double Submit Cookie в React и Vue.js



Паттерн Double Submit Cookie — один из самых надежных способов защиты от CSRF-атак в современных веб-приложениях. Его суть заключается в отправке одного и того же токена как в куки, так и в теле запроса или заголовке. Поскольку вредоносные сайты не могут читать содержимое куки из-за ограничений Same-Origin Policy, они не могут включить правильный токен в запрос. В контексте современных фреймворков вроде React и Vue.js реализация этого паттерна имеет свои особенности. Давайте рассмотрим, как его можно имплементировать в каждом из них.

Реализация в React



В React-приложениях чаще всего используются библиотеки типа axios или fetch API для выполнения HTTP-запросов. Вот как можно реализовать Double Submit Cookie с использованием хуков:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Хук для работы с CSRF-защитой
function useCsrfProtection() {
  const [csrfToken, setCsrfToken] = useState(null);
  
  // Получаем токен при первой загрузке компонента
  useEffect(() => {
    // Функция для извлечения значения куки по имени
    function getCookie(name) {
      const value = `; ${document.cookie}`;
      const parts = value.split(`; ${name}=`);
      if (parts.length === 2) return parts.pop().split(';').shift();
    }
    
    // Получаем токен из куки
    const tokenFromCookie = getCookie('csrf_token');
    
    if (tokenFromCookie) {
      setCsrfToken(tokenFromCookie);
    } else {
      // Если токена нет в куки, запрашиваем новый
      fetch('/api/csrf-token', { credentials: 'include' })
        .then(response => response.json())
        .then(data => {
          setCsrfToken(data.token);
        })
        .catch(error => {
          console.error('Ошибка получения CSRF-токена:', error);
        });
    }
  }, []);
  
  // Функция для выполнения защищенных запросов
  const fetchWithCsrf = useCallback((url, options = {}) => {
    if (!csrfToken) {
      return Promise.reject(new Error('CSRF токен не найден'));
    }
    
    const headers = {
      ...options.headers,
      'X-CSRF-Token': csrfToken
    };
    
    return fetch(url, {
      ...options,
      headers,
      credentials: 'include' // Важно для отправки куки
    });
  }, [csrfToken]);
  
  return { csrfToken, fetchWithCsrf };
}
Теперь можно использовать этот хук в компонентах:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function UserProfile() {
  const { fetchWithCsrf } = useCsrfProtection();
  const [profile, setProfile] = useState(null);
  
  const updateProfile = async (newData) => {
    try {
      const response = await fetchWithCsrf('/api/user/profile', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(newData)
      });
      
      if (!response.ok) throw new Error('Ошибка обновления профиля');
      
      const updatedProfile = await response.json();
      setProfile(updatedProfile);
    } catch (error) {
      console.error('Не удалось обновить профиль:', error);
    }
  };
  
  // Остальной код компонента...
}
При использовании Redux и централизованного управления состоянием, можно создать промежуточное ПО (middleware) для автоматического добавления CSRF-токена во все запросы:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Redux middleware для CSRF-защиты
const csrfMiddleware = store => next => action => {
  if (action.type === 'API_REQUEST') {
    const csrfToken = document.cookie
      .split('; ')
      .find(row => row.startsWith('csrf_token='))
      ?.split('=')[1];
    
    if (csrfToken) {
      action.payload.headers = {
        ...action.payload.headers,
        'X-CSRF-Token': csrfToken
      };
    }
  }
  
  return next(action);
};

Реализация в Vue.js



Vue.js имеет свои особенности, но общий принцип остается тем же. Вот как можно реализовать Double Submit Cookie в Vue.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// src/plugins/csrf.js
export default {
  install(app) {
    const csrfToken = getCookie('csrf_token');
    
    // Если токена нет, запрашиваем его
    if (!csrfToken) {
      fetch('/api/csrf-token', { credentials: 'include' })
        .then(response => response.json())
        .then(data => {
          // Токен автоматически устанавливается как куки сервером
          console.log('Получен новый CSRF-токен');
        });
    }
    
    // Создаем глобальную функцию для защищенных запросов
    app.config.globalProperties.$fetchWithCsrf = function(url, options = {}) {
      const token = getCookie('csrf_token');
      
      if (!token) {
        return Promise.reject(new Error('CSRF токен не найден'));
      }
      
      const headers = {
        ...options.headers,
        'X-CSRF-Token': token
      };
      
      return fetch(url, {
        ...options,
        headers,
        credentials: 'include'
      });
    };
    
    // Добавляем утилиту для получения токена в компонентах
    app.config.globalProperties.$getCsrfToken = function() {
      return getCookie('csrf_token');
    };
  }
};
 
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
  return null;
}
Регистрируем плагин при инициализации Vue-приложения:

JavaScript
1
2
3
4
5
6
7
8
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import csrfPlugin from './plugins/csrf';
 
const app = createApp(App);
app.use(csrfPlugin);
app.mount('#app');
Теперь в любом компоненте можно использовать метод $fetchWithCsrf:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
  <form @submit.prevent="updateProfile">
    <!-- Форма редактирования профиля -->
    <button type="submit">Сохранить</button>
  </form>
</template>
 
<script>
export default {
  data() {
    return {
      profile: {
        name: '',
        email: ''
      }
    };
  },
  methods: {
    async updateProfile() {
      try {
        const response = await this.$fetchWithCsrf('/api/user/profile', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(this.profile)
        });
        
        if (!response.ok) throw new Error('Ошибка обновления профиля');
        
        const result = await response.json();
        this.$emit('updated', result);
      } catch (error) {
        console.error('Не удалось обновить профиль:', error);
      }
    }
  }
};
</script>
При использовании Vuex для управления состоянием можно создать действие (action), которое автоматически добавляет CSRF-токен:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// store/actions.js
export default {
  async apiRequest({ commit }, { url, method = 'GET', data = null }) {
    const csrfToken = document.cookie
      .split('; ')
      .find(row => row.startsWith('csrf_token='))
      ?.split('=')[1];
    
    const options = {
      method,
      headers: {
        'Content-Type': 'application/json'
      },
      credentials: 'include'
    };
    
    if (csrfToken) {
      options.headers['X-CSRF-Token'] = csrfToken;
    }
    
    if (data) {
      options.body = JSON.stringify(data);
    }
    
    try {
      const response = await fetch(url, options);
      return await response.json();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }
}
В обоих фреймворках важно настроить серверную часть для правильной обработки Double Submit Cookie. На бэкенде нужно:
1. Генерировать случайный токен.
2. Устанавливать его как куки.
3. Проверять, совпадает ли токен в куки с токеном в заголовке запроса.

Этот паттерн прекрасно защищает от CSRF-атак, особенно в сочетании с другими мерами безопасности, такими как SameSite куки и строгая проверка Content-Type.

SQL-инъекции в JavaScript-контексте



В мире JavaScript разработки SQL-инъекции часто недооценивают. "У нас же Node.js и веб-приложение, какие SQL-инъекции?" — типичное заблуждение. Реальность куда тревожнее: современные JavaScript-приложения, особенно на Node.js, часто работают непосредственно с базами данных, что открывает двери для этого класса атак. Суть SQL-инъекции предельно проста — злоумышленник внедряет вредоносный SQL-код в запрос через пользовательский ввод. В контексте JavaScript наибольшему риску подвержены серверные приложения на Node.js, использующие прямые SQL-запросы к базам данных. Вот типичный пример уязвимого кода:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Опасный код, уязвимый к SQL-инъекции
const express = require('express');
const mysql = require('mysql');
const app = express();
 
app.use(express.json());
 
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'users_db'
});
 
app.get('/users', (req, res) => {
  const username = req.query.username;
  // Уязвимый запрос!
  const query = `SELECT * FROM users WHERE username = '${username}'`;
  
  connection.query(query, (err, results) => {
    if (err) throw err;
    res.json(results);
  });
});
 
app.listen(3000);
Казалось бы, простой поиск пользователя по имени. Но если злоумышленник отправит запрос вроде ?username=admin' OR '1'='1, итоговый SQL-запрос превратится в:

SQL
1
SELECT * FROM users WHERE username = 'admin' OR '1'='1'
Условие '1'='1' всегда истинно, поэтому запрос вернёт всех пользователей из базы — гораздо больше, чем задумывалось изначально.

А что насчет более опасных атак? Представим запрос вида ?username=dummy'; DROP TABLE users; --:

SQL
1
SELECT * FROM users WHERE username = 'dummy'; DROP TABLE users; --'
Теперь у нас цепочка из двух запросов, где второй удаляет всю таблицу пользователей. Последовательность -- комментирует оставшуюся часть запроса, чтобы избежать синтаксических ошибок. Как защититься? Первый и главный способ — использовать параметризованные запросы:

JavaScript
1
2
3
4
5
6
7
8
9
10
app.get('/users', (req, res) => {
  const username = req.query.username;
  // Безопасный параметризованный запрос
  const query = `SELECT * FROM users WHERE username = ?`;
  
  connection.query(query, [username], (err, results) => {
    if (err) throw err;
    res.json(results);
  });
});
В этом случае драйвер базы данных обрабатывает входные данные как значения, а не как часть SQL-кода, что предотвращает инъекцию. Заметьте, что я использую метод query с массивом параметров вторым аргументом, а не простую конкатенацию строк.
Другой подход — использование ORM (Object-Relational Mapping) библиотек, которые абстрагируют SQL-запросы и автоматически применяют параметризацию:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { Sequelize, Model, DataTypes } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');
 
class User extends Model {}
User.init(
  { username: DataTypes.STRING },
  { sequelize, modelName: 'user' }
);
 
app.get('/users', async (req, res) => {
  const username = req.query.username;
  // Sequelize автоматически защищает от SQL-инъекций
  const users = await User.findAll({
    where: { username }
  });
  
  res.json(users);
});
Sequelize, TypeORM, Prisma и другие ORM не только повышают безопасность, но и делают код более понятным и поддерживаемым.
Если же использование ORM невозможно, вот несколько дополнительных мер защиты:

1. Валидация входных данных — проверяйте типы и форматы входных данных:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function isValidUsername(username) {
  // Только буквы и цифры, длина 3-20 символов
  return /^[a-zA-Z0-9]{3,20}$/.test(username);
}
 
app.get('/users', (req, res) => {
  const username = req.query.username;
  
  if (!isValidUsername(username)) {
    return res.status(400).json({ error: 'Invalid username' });
  }
  
  // Продолжаем с безопасным запросом...
});
2. Экранирование специальных символов — если параметризация недоступна:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function escapeSQL(value) {
  return String(value)
    .replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) {
      switch(s) {
        case "\0": return "\\0";
        case "\n": return "\\n";
        case "\r": return "\\r";
        case "\b": return "\\b";
        case "\t": return "\\t";
        case "\x1a": return "\\Z";
        case "'": return "''";
        case '"': return '""';
        default: return "\\" + s;
      }
    });
}
3. Принцип наименьших привилегий — используйте для подключения к БД пользователя с минимально необходимыми правами:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Пользователь только с правами чтения
const readOnlyConnection = mysql.createConnection({
  user: 'app_readonly',
  password: '[B][/B]',
  // Другие параметры...
});
 
// Пользователь с правами записи
const writeConnection = mysql.createConnection({
  user: 'app_readwrite',
  password: '[B][/B]',
  // Другие параметры...
});
 
// Используем соответствующее подключение в зависимости от операции
SQL-инъекции в JavaScript-приложениях не так очевидны, как в PHP или других "старых" технологиях, но они столь же опасны и требуют особого внимания. Помните: любое место, где пользовательский ввод встречается с запросом к базе данных — потенциальная точка компрометации вашего приложения.

Уязвимости NoSQL-инъекций в MongoDB и методы их предотвращения



Говоря о безопасности JavaScript-приложений, нельзя обойти вниманием NoSQL базы данных, особенно MongoDB, которая стала стандартом де-факто для многих Node.js-проектов. Распространено заблуждение, что NoSQL базы данных защищены от инъекций по своей природе. Это опасный миф, который я не раз наблюдал в реальных проектах. NoSQL-инъекции отличаются от классических SQL-инъекций, но не менее разрушительны. Вместо внедрения SQL-кода, злоумышленник манипулирует структурой JSON-подобных запросов, часто используя операторы MongoDB для получения несанкционированного доступа к данным. Рассмотрим типичный пример уязвимого кода при работе с MongoDB в Node.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Уязвимый запрос!
  const user = await db.collection('users').findOne({
    username: username, 
    password: password
  });
  
  if (user) {
    // Аутентификация успешна
    res.json({ success: true, token: generateToken(user) });
  } else {
    res.status(401).json({ success: false });
  }
});
Казалось бы, что может пойти не так? Но представьте, что злоумышленник отправляет такие данные:

JSON
1
2
3
4
{
  "username": "admin",
  "password": { "$ne": null }
}
MongoDB интерпретирует оператор $ne (не равно), и запрос превращается в поиск пользователя с именем "admin" и паролем, не равным null. Если такой пользователь существует, аутентификация успешно пройдена без знания правильного пароля!

А вот еще более коварный пример с оператором $gt (больше чем):

JSON
1
2
3
4
{
  "username": { "$gt": "" },
  "password": { "$gt": "" }
}
Этот запрос потенциально вернет первого пользователя из коллекции, так как любая строка "больше" пустой строки в лексикографическом порядке.
Как защитить MongoDB-приложения от NoSQL-инъекций?

1. Валидация и типизация входных данных
Первая и самая важная линия защиты — строгая валидация типов входных данных:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Проверяем, что username и password - строки
  if (typeof username !== 'string' || typeof password !== 'string') {
    return res.status(400).json({ error: 'Invalid input' });
  }
  
  // Безопасный запрос с проверенными данными
  const user = await db.collection('users').findOne({
    username, password
  });
  
  // Остальная логика...
});
2. Использование ODM (Object-Document Mapper)
ODM-библиотеки вроде Mongoose предоставляют дополнительный уровень абстракции и защиты:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const UserSchema = new mongoose.Schema({
  username: String,
  password: String
});
 
const User = mongoose.model('User', UserSchema);
 
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Mongoose автоматически проверяет типы
  const user = await User.findOne({ username, password });
  
  // Остальная логика...
});
3. Экранирование специальных операторов MongoDB
Если вам нужно принимать сложные объекты от пользователя, рекурсивно проверяйте их на наличие операторов MongoDB:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function sanitizeObject(obj) {
  if (!obj || typeof obj !== 'object') {
    return obj;
  }
  
  const result = {};
  
  // Обходим все свойства объекта
  for (const key in obj) {
    // Отфильтровываем ключи, начинающиеся с $
    if (key.startsWith('$')) {
      continue;
    }
    
    // Рекурсивно обрабатываем вложенные объекты
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      result[key] = sanitizeObject(obj[key]);
    } else {
      result[key] = obj[key];
    }
  }
  
  return result;
}
 
// Использование
app.post('/api/search', async (req, res) => {
  const filter = sanitizeObject(req.body.filter || {});
  const results = await db.collection('items').find(filter).toArray();
  res.json(results);
});
4. Использование проекций для ограничения возвращаемых полей
Ограничивайте данные, которые возвращаются запросом:

JavaScript
1
2
3
4
5
6
7
8
app.get('/api/users', async (req, res) => {
  // Возвращаем только безопасные поля, исключая пароль
  const users = await db.collection('users').find({}, { 
    projection: { username: 1, email: 1, _id: 1 } 
  }).toArray();
  
  res.json(users);
});
5. Агрегации и сложные запросы
Особенно осторожным нужно быть с агрегационным конвейером MongoDB. Пользовательский ввод никогда не должен напрямую включаться в стадии агрегации:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
// Опасно
const pipeline = [
  { $match: req.body.match },
  { $group: req.body.group }
];
 
// Безопасно
const pipeline = [
  { $match: { status: req.query.status === 'active' ? 'active' : 'inactive' } },
  { $group: { _id: "$category", count: { $sum: 1 } } }
];
6. Принцип наименьших привилегий
Создавайте отдельных пользователей MongoDB с минимально необходимыми правами для различных операций:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Роли можно настроить в MongoDB
// db.createUser({
//   user: "readOnlyUser",
//   pwd: "password",
//   roles: [{ role: "read", db: "myDb" }]
// })
 
// Затем использовать соответствующего пользователя для подключения
const readOnlyClient = new MongoClient(uri, {
  auth: {
    username: 'readOnlyUser',
    password: 'password'
  }
});
NoSQL-инъекции могут быть менее известны, чем их SQL-аналоги, но они представляют серьезную угрозу для Node.js-приложений с MongoDB. Комбинируя строгую валидацию типов, ODM-библиотеки и тщательное экранирование пользовательского ввода, можно значительно снизить риск этих атак.

Защита GraphQL API от инъекций и эксплойтов с примерами



GraphQL становится всё популярнее в JavaScript-экосистеме благодаря гибкости и эффективности при работе с данными. Однако эта же гибкость создает специфические уязвимости отличающиеся от традиционных REST API. В отличие от SQL или NoSQL инъекций, атаки на GraphQL принимают более изощренные формы, такие как рекурсивные запросы, слишком сложные запросы или абьюз батчинга.

Основные уязвимости GraphQL API и методы защиты:

1. Deep Nested Queries (Глубоко вложенные запросы)
GraphQL позволяет делать запросы с произвольной вложенностью, что может привести к DoS-атакам:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
query {
  user {
    friends {
      friends {
        friends {
          friends {
            # И так до бесконечности...
          }
        }
      }
    }
  }
}
Такие запросы могут привести к экспоненциальному росту количества запросов к базе данных. Решение — ограничение глубины:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
const { createComplexityLimitRule } = require('graphql-validation-complexity');
 
const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10
    })
  ]
});
2. Field Suggestion Attacks (Атаки на подсказки полей)
По умолчанию GraphQL предоставляет информацию об ошибках и интроспекцию схемы, что облегчает работу злоумышленника:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
query {
  __schema {
    types {
      name
      fields {
        name
        type {
          name
        }
      }
    }
  }
}
Для защиты можно отключить интроспекцию в продакшн-среде:

JavaScript
1
2
3
4
5
6
7
8
9
const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
  validationRules: [
    // Отключаем интроспекцию по условию
    process.env.NODE_ENV === 'production' 
      ? NoSchemaIntrospectionRule : () => true
  ]
});
3. Injection in Arguments (Инъекции в аргументах)
Хотя GraphQL сам по себе не подвержен SQL-инъекциям, уязвимость может возникнуть в резолверах:

JavaScript
1
2
3
4
5
6
7
8
9
// Уязвимый резолвер
const resolvers = {
  Query: {
    users: (_, args) => {
      const query = `SELECT * FROM users WHERE role = '${args.role}'`;
      return db.raw(query);
    }
  }
};
Решение — использовать параметризованные запросы и валидацию:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
const resolvers = {
  Query: {
    users: async (_, args) => {
      // Валидируем аргументы
      if (!['admin', 'user', 'guest'].includes(args.role)) {
        throw new Error('Invalid role');
      }
      // Используем параметризованные запросы
      return db('users').where({ role: args.role });
    }
  }
};
4. Batch Attacks (Атаки через пакетную обработку)
Злоумышленники могут отправлять множество запросов в одном HTTP-запросе:

JSON
1
2
3
4
5
6
[
  { "query": "query { user(id: 1) { name } }" },
  { "query": "query { user(id: 2) { name } }" },
  ...
  // Сотни или тысячи запросов
]
Защитные меры включают ограничение количества операций в одном запросе:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
app.use('/graphql', (req, res, next) => {
  // Если это батч запрос
  if (Array.isArray(req.body)) {
    // Ограничиваем количество запросов
    if (req.body.length > 10) {
      return res.status(400).json({
        errors: [{ message: 'Too many operations in single request' }]
      });
    }
  }
  next();
});
5. Query Persistence (Персистентные запросы)
Вместо прямой передачи запросов GraphQL поддерживает подход с хешированием и хранением запросов на сервере:

JavaScript
1
2
3
4
5
6
7
8
9
// Клиент отправляет только хеш
fetch('/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    documentId: 'HASH_OF_PREDEFINED_QUERY',
    variables: { userId: 123 }
  })
});
На сервере:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const persistedQueries = {
  'HASH_OF_PREDEFINED_QUERY': `
    query GetUser($userId: ID!) {
      user(id: $userId) {
        name
        email
      }
    }
  `
};
 
app.use('/graphql', (req, res, next) => {
  if (req.body.documentId) {
    req.body.query = persistedQueries[req.body.documentId];
    if (!req.body.query) {
      return res.status(400).json({
        errors: [{ message: 'Unknown query id' }]
      });
    }
  }
  next();
});
6. Защита на уровне типов и полей
Современные GraphQL-серверы позволяют реализовать детальный контроль доступа:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const typeDefs = gql`
  type User {
    id: ID!
    email: String! @auth(requires: ADMIN)
    name: String!
    role: String!
  }
`;
 
// Добавляем директиву для контроля доступа
const resolvers = {
  User: {
    email: (user, _, context) => {
      if (context.user && context.user.role === 'ADMIN') {
        return user.email;
      }
      throw new Error('Not authorized');
    }
  }
};
Инструменты вроде graphql-shield упрощают эту задачу:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { shield, rule, and, or, not } = require('graphql-shield');
 
const isAuthenticated = rule()((_, __, context) => {
  return Boolean(context.user);
});
 
const isAdmin = rule()((_, __, context) => {
  return context.user && context.user.role === 'ADMIN';
});
 
const permissions = shield({
  Query: {
    users: isAdmin,
    user: isAuthenticated
  },
  User: {
    email: isAdmin,
    settings: isAuthenticated
  }
});
 
// Применяем middleware к схеме
const schemaWithPermissions = applyMiddleware(schema, permissions);
7. Rate Limiting (Ограничение частоты запросов)
Для защиты от DoS-атак необходимо ограничивать частоту запросов:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const rateLimiter = require('graphql-rate-limit');
 
const limiter = rateLimiter({
  identifyContext: (ctx) => ctx.ip,
  formatError: () => 'Слишком много запросов, попробуйте позднее'
});
 
const resolvers = {
  Query: {
    sensitiveData: limiter({
      window: '1m',
      max: 10
    }, (_, args, context) => {
      return fetchSensitiveData();
    })
  }
};
Защита GraphQL API требует многоуровневого подхода — от правильной настройки резолверов до внедрения общих ограничений на уровне сервера. Только комбинация этих методов обеспечит надежную защиту от большинства известных эксплойтов.

Валидация данных на стороне клиента как первая линия обороны



Клиентская валидация данных часто недооценивается в контексте безопасности приложений. Многие разработчики воспринимают её исключительно как способ улучшения пользовательского опыта, забывая о защитном потенциале. На деле же тщательная проверка данных в браузере — это первый рубеж обороны против многих типов атак.

Был случай, когда один из моих клиентов категорически отвергал необходимость двойной валидации: "Зачем проверять данные дважды? Это дублирование кода!" Через месяц после запуска проекта база данных оказалась забита мусором из-за специально сформированных запросов, обходивших только серверную валидацию. Урок был болезненным.

Клиентская валидация выполняет две важные функции:
1. Фильтрует очевидно некорректные данные, снижая нагрузку на сервер.
2. Обеспечивает мгновенную обратную связь для пользователей, улучшая UX.

При этом важно понимать: валидация на стороне клиента никогда не заменяет серверную проверку. Любой злоумышленник может обойти клиентские ограничения, используя инструменты разработчика или прямые запросы к API. Тем не менее, грамотно реализованная клиентская валидация существенно повышает общий уровень безопасности.

Основные стратегии валидации в JavaScript:

Валидация форм с помощью встроенных возможностей HTML5



HTML5 предоставляет ряд атрибутов для базовой валидации:

HTML5
1
2
3
4
5
6
<form>
  <input type="email" required pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$">
  <input type="text" minlength="8" maxlength="20">
  <input type="number" min="1" max="100">
  <button type="submit">Отправить</button>
</form>
Преимущество такого подхода — браузер сам блокирует отправку формы с некорректными данными. Однако этих инструментов часто недостаточно для сложной валидации.

JavaScript-валидация в реальном времени



Более гибкий подход — проверка данных с помощью JavaScript:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const emailInput = document.getElementById('email');
const emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/;
 
emailInput.addEventListener('input', function() {
  if (!emailRegex.test(this.value)) {
    this.setCustomValidity('Пожалуйста, введите корректный email');
  } else {
    this.setCustomValidity('');
  }
});
 
form.addEventListener('submit', function(e) {
  const username = document.getElementById('username').value;
  
  // Проверка на потенциально опасные символы
  if (/[<>'"&]/.test(username)) {
    e.preventDefault();
    alert('Имя пользователя содержит недопустимые символы');
  }
});
Такой подход позволяет реализовать кастомную логику и давать пользователям подробную обратную связь.

Валидация в React-приложениях



В React часто используются специализированные библиотеки для валидации форм:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
 
// Определение схемы валидации
const schema = yup.object().shape({
  username: yup.string()
    .required('Имя пользователя обязательно')
    .matches(/^[a-zA-Z0-9_]+$/, 'Только буквы, цифры и подчёркивания')
    .min(3, 'Минимум 3 символа')
    .max(20, 'Максимум 20 символов'),
  email: yup.string()
    .required('Email обязателен')
    .email('Введите корректный email'),
  password: yup.string()
    .required('Пароль обязателен')
    .min(8, 'Минимум 8 символов')
    .matches(/[0-9]/, 'Должен содержать цифру')
    .matches(/[A-Z]/, 'Должен содержать заглавную букву')
});
 
function RegistrationForm() {
  const { register, handleSubmit, errors } = useForm({
    resolver: yupResolver(schema)
  });
  
  const onSubmit = data => {
    // Данные прошли валидацию, можно отправлять на сервер
    api.register(data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="username" ref={register} />
      {errors.username && <p>{errors.username.message}</p>}
      
      <input name="email" ref={register} />
      {errors.email && <p>{errors.email.message}</p>}
      
      <input name="password" type="password" ref={register} />
      {errors.password && <p>{errors.password.message}</p>}
      
      <button type="submit">Зарегистрироваться</button>
    </form>
  );
}
Yup и другие библиотеки схем позволяют создавать сложные правила валидации, которые защищают от большинства типичных проблем.

Предотвращение XSS через валидацию



Особое внимание стоит уделять валидации полей, которые могут содержать HTML или код:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function validateUserInput(input) {
  // Проверка на потенциальные XSS-векторы
  if (/<script|javascript:|on\w+=|data:/i.test(input)) {
    return {
      valid: false,
      message: 'Обнаружен потенциально опасный контент'
    };
  }
  
  // Ограничение длины для предотвращения DoS-атак
  if (input.length > 1000) {
    return {
      valid: false,
      message: 'Слишком длинный текст'
    };
  }
  
  return { valid: true };
}
 
// Использование в обработчике формы
submitButton.addEventListener('click', () => {
  const commentText = document.getElementById('comment').value;
  const validation = validateUserInput(commentText);
  
  if (!validation.valid) {
    showError(validation.message);
    return;
  }
  
  // Отправка данных на сервер
});
Серверная проверка всегда должна быть реализована как основной защитный механизм. Тем не менее, правильно настроенная фронтенд-валидация существенно снижает поверхность атаки и делает вашу систему защиты многоуровневой и более надёжной. В конечном итоге, валидация данных на стороне клиента — это не только вопрос удобства использования, но и важный компонент комплексной стратегии безопасности. Она блокирует многие примитивные атаки, снижает нагрузку на серверную инфраструктуру и предоставляет пользователям мгновенную обратную связь о проблемах в их данных.

Автоматизация безопасности



Ручное тестирование и мониторинг уже не справляются с темпами разработки и количеством потенциальных уязвимостей. Для JavaScript-приложений это особенно актуально из-за быстрой эволюции фреймворков и библиотек. Начнём с самого простого — статического анализа кода. Инструменты вроде ESLint с плагинами безопасности могут выявлять потенциально опасные паттерны прямо во время разработки:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Настройка ESLint с правилами безопасности
module.exports = {
  plugins: ['security'],
  extends: [
    'plugin:security/recommended',
  ],
  rules: {
    'security/detect-non-literal-regexp': 'error',
    'security/detect-unsafe-regex': 'error',
    'security/detect-buffer-noassert': 'error',
    'security/detect-eval-with-expression': 'error',
    'security/detect-no-csrf-before-method-override': 'error',
  }
};
Подобная конфигурация предупредит о многих типичных ошибках ещё на этапе написания кода, что гораздо дешевле исправления на продакшене.
Следующий шаг — автоматический аудит зависимостей. Уязвимости чаще всего проникают в проект через сторонние библиотеки. Здесь на помощь приходят инструменты вроде npm audit, Snyk, или OWASP Dependency Check:

Bash
1
2
3
4
# Автоматическая проверка зависимостей
npm audit
# Или более продвинутый вариант
snyk test
Интеграция таких инструментов в процесс сборки предотвращает внедрение известных уязвимостей.
Для обнаружения XSS, CSRF и инъекций на работающем приложении эффективны автоматические сканеры безопасности. Инструменты вроде OWASP ZAP, Burp Suite или Arachni можно настроить на регулярное сканирование:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Пример автоматизации тестирования с использованием ZAP API
const ZapClient = require('zaproxy');
 
const zaproxy = new ZapClient({
  apiKey: 'api-key-here',
  proxy: 'http://localhost:8080'
});
 
// Запуск активного сканирования
async function runScan() {
  try {
    await zaproxy.spider.scan('https://example.com');
    const scanId = await zaproxy.ascan.scan('https://example.com');
    console.log('Scan started with ID:', scanId);
    
    // Проверяем результаты
    setTimeout(async () => {
      const alerts = await zaproxy.core.alerts();
      console.log('Обнаружены уязвимости:', alerts.alerts.length);
    }, 300000); // Ждём завершения
  } catch (error) {
    console.error('Ошибка сканирования:', error);
  }
}
Автоматизированное тестирование на проникновение (pen testing) — еще более продвинутый подход, где специальные инструменты не просто обнаруживают, но и пытаются эксплуатировать уязвимости. Такие тесты стоит проводить в изолированной среде, имитирующей продакшн.

Автоматизация тестирования безопасности в CI/CD-пайплайнах



Включение проверок безопасности в процесс непрерывной интеграции и доставки (CI/CD) — критически важный шаг для современной разработки. Когда безопасность интегрирована в пайплайны, команды получают мгновенную обратную связь о потенциальных уязвимостях прямо в процессе разработки, а не постфактум.

Типичный пайплайн безопасности для JavaScript-приложения может включать следующие этапы:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Пример конфигурации для GitHub Actions
name: Security Pipeline
 
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
 
jobs:
  security-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      # Статический анализ кода
      - name: ESLint Security
        run: npx eslint . --ext .js,.jsx --config security-eslint.json
        
      # Проверка зависимостей
      - name: Dependency Check
        run: npm audit --audit-level=high
        
      # SAST - статический анализ безопасности
      - name: Run NodeJsScan
        uses: ajinabraham/njsscan-action@master
        with:
          args: '.'
      
      # SCA - анализ компонентов
      - name: Snyk
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
Важно настроить уровни критичности правильно. На старте я рекомендую не блокировать сборку на предупреждениях средней и низкой важности, иначе команда быстро выработает "усталость от предупреждений" и начнет игнорировать реальные проблемы.
Для более сложных сценариев можно добавить динамический анализ — запуск приложения в тестовой среде с последующим сканированием:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dynamic-security:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - name: Deploy to Test Environment
      # Деплой в тестовую среду
      
    - name: Run OWASP ZAP Scan
      uses: zaproxy/action-full-scan@v0.4.0
      with:
        target: 'https://test-environment.example.com'
        rules_file_name: 'zap-rules.tsv'
        
    - name: Archive Security Reports
      uses: actions/upload-artifact@v3
      with:
        name: security-reports
        path: reports/
Критический момент — обработка результатов тестирования. Простое накопление отчетов без действий бесполезно. Эффективный подход включает:
1. Автоматическое создание тикетов для критических уязвимостей.
2. Информационную панель с трендами безопасности.
3. Регулярные обзоры накопленных данных.

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Скрипт для обработки отчетов и создания тикетов
const axios = require('axios');
const fs = require('fs');
 
// Чтение отчета о безопасности
const securityReport = JSON.parse(fs.readFileSync('zap-report.json'));
 
// Фильтрация критических проблем
const criticalIssues = securityReport.issues.filter(
  issue => issue.severity === 'High' || issue.severity === 'Critical'
);
 
// Создание тикетов
async function createTickets() {
  for (const issue of criticalIssues) {
    await axios.post('https://your-issue-tracker-api/issues', {
      title: [INLINE]Security Issue: ${issue.name}[/INLINE],
      description: [INLINE]${issue.description}\n\nLocation: ${issue.location}\nSeverity: ${issue.severity}[/INLINE],
      type: 'security',
      priority: issue.severity === 'Critical' ? 'Blocker' : 'High'
    });
  }
}
 
createTickets().catch(console.error);
Не все проверки должны выполняться на каждом коммите — это может существенно замедлить процесс разработки. Практичный подход — разные уровни проверок для разных событий:
  • Базовые проверки (линтинг, npm audit) на каждый коммит.
  • Полный статический анализ при создании PR.
  • Сканирование зависимостей раз в день.
  • Полное динамическое сканирование перед релизом.

Такая стратегия обеспечивает баланс между безопасностью и скоростью разработки.

Защита от sql инъекций
Добрый день, коллеги в разрабатываемом клиент-серверном приложении (vb.net + ms sql) работа с данными БД происходит через запросы, формируемые...

Защититься от SQL инъекций
Добрый вечер, Форумчане Не подскажите достаточно ли данных мер для защиты от SQL-инъекций? if(isset($_POST)){ //Экранируем...

Защита от SQL инъекций
Всем добрый день. Я второй день изучаю php в связи с тем, что мне нужно срочно написать скрипт голосования. Идея такая: Пользователь переходит в топ...

Защита от SQL инъекций
Здравствуйте! Прошерстив 2 дня интернет так и не нашел ответ на свой вопрос. Например если я знаю какие мне данные отправит пользователь, то я...

защита от sql инъекций
Добрый день! Подскажите пожалуйста, от sql инъекций спасает ли mysql_real_escape_string? есть ли что-нибудь еще для этого?

Защита от sql инъекций
Есть сайт, на его странице через метод GET передаётся параметр переменной id записи в таблице. Дальше всё просто SELECT * FROM `table` WHERE...

Защита от SQL инъекций
Тема избитая. По ней куча статей, но информация зачастую противоречивая. Во-первых, вопрос - является ли слэширование (' -&gt; \', &quot; -&gt;...

Защита от SQL инъекций-PDO
Прочитал,что для защиты от инъекций нужно использовать PDO. Если переменная является параметром в запросе,то всё нормально, $search=$_POST; ...

Защита от sql инъекций cookie
Когда авторизированный пользователь шагает по сайту у него каждый раз проверяется куки(записывается онлайн, проверяется на существование(в случае...

сканер на наличие sql инъекций
Приветствую Получаю все ссылки с сайта с помощью регулярного выражения и привожу их в единый вид, заношу получившиеся ссылки в массив. ...

Не работает защита от sql инъекций
Здравствуйте!. Есть следующий код. Решил сделать защиту от sql инъекций , но почему-то не работает execute(). За помощь очень благодарен &lt;?php ...

Трудности с SQL-инъекцией и XSS
Вечер добрый, форумчане! Помогите советом, есть несколько вопросов. 1. Есть скрипт авторизации - форма &lt;form...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Циклы for в Python
py-thonny 17.03.2025
Существует множество ситуаций, когда нам нужно выполнить одно и то же действие несколько раз. Цикл for в Python — настоящий рабочий конь для большинства программистов. Если вам нужно пройтись по всем. . .
Предсказание ветвлений - путь к высокопроизводи­тельному C++
NullReferenced 17.03.2025
В высокопроизводительном программировании на C++ каждый такт процессора на счету. Когда речь заходит о разработке систем с низкой задержкой — будь то высокочастотная торговля, обработка потоковых. . .
Паттерн CQRS в C#
UnmanagedCoder 17.03.2025
Создание сложных корпоративных приложений часто требует нестандартных подходов к архитектуре. Один из таких подходов — паттерн CQRS (Command Query Responsibility Segregation), предлагающий простую,. . .
Паттерн Цепочка ответственности в C#
UnmanagedCoder 17.03.2025
Цепочка ответственности — это поведенческий паттерн проектирования, который позволяет передавать запросы последовательно по цепочке потенциальных обработчиков, пока один из них не обработает запрос. . . .
Создаем микросервисы с NestJS, TCP и Typescript
run.dev 17.03.2025
NestJS — фреймворк, который значительно упрощает создание серверных приложений на Node. js. Его прелесть в том, что он комбинирует концепции ООП, функционального программирования и предлагает. . .
Гексагональная архитектура со Spring Boot
Javaican 17.03.2025
Если вы когда-нибудь сталкивались с ситуацией, когда внесение простых изменений в базу данных или пользовательский интерфейс заставляло вас переписывать весь код, то вы точно оцените элегантность. . .
Позиционировани­е Kafka Consumer и Seek-операции
Javaican 17.03.2025
Что же такое Consumer Seek в Kafka? По сути, это API-метод, который позволяет программно указать, с какой позиции (offset) Consumer должен начать или продолжить чтение данных из партиции. Без этого. . .
Python NumPy: Лучшие практики и примеры
py-thonny 17.03.2025
NumPy (Numerical Python) — одна из ключевых библиотек для научных вычислений в Python. Она превращает Python из просто удобного языка общего назначения в среду для проведения сложных математических. . .
Java Micronaut в Docker: контейнеризация с Maven и Jib
Javaican 16.03.2025
Когда речь заходит о микросервисной архитектуре на Java, фреймворк Micronaut выделяется среди конкурентов. Он создан с учётом особенностей облачных сред и контейнеров, что делает его идеальным. . .
Управление зависимостями в Java: Сравнение Spring, Guice и Dagger 2
Javaican 16.03.2025
Инъекция зависимостей (Dependency Injection, DI) — один из фундаментальных паттернов проектирования, который радикально меняет подход к созданию гибких и тестируемых Java-приложений. Суть этого. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru