
Статья подготовлена вместе с экспертом
Станислав Погоржельский, технологический евангелист VK Cloud&Data

Атака на MOVEit Transfer в 2023 году выглядела почти издевательски просто: группа Cl0p воспользовалась SQL-инъекцией, получила доступ к данным 93,3 млн человек и задела BBC, British Airways, HSBC и McDonald's одним эксплойтом. Это уязвимость, известная уже 25 лет, и имеющая надёжные методы защиты — просто ее по-прежнему иногда не закрывают.
XSS и SQL-инъекции возглавляют MITRE CWE Top 25 за 2025 год — первое и второе место из 39 080 проанализированных CVE. В проекте OWASP Top 10:2025 Injection занимает позицию A05. То есть это по-прежнему актуальная и реальная угроза, и игнорировать её попросту нельзя.
Статья адресована веб-разработчикам на Python, PHP, JavaScript и Java, а также AppSec-инженерам. Разберём, как работают SQL-инъекции и XSS, покажем уязвимый и защищённый код, и сведём защиту в чек-лист, который можно прогнать по продакшену уже сегодня.

Станислав Погоржельский, технологический евангелист VK Cloud&Data
Для начала разберём все важные термины, которые вы встретите далее, чтобы статья читалась проще.
CVE (Common Vulnerabilities and Exposures) — международный реестр публично известных уязвимостей в программном обеспечении; каждой присваивается уникальный идентификатор вида CVE-YYYY-NNNNN.
CWE (Common Weakness Enumeration) — классификатор типовых уязвимостей и слабостей программного обеспечения, разрабатываемый организацией MITRE.
MITRE CWE Top 25 — ежегодный рейтинг 25 наиболее опасных и распространённых уязвимостей, составляемый MITRE на основе анализа реальных CVE.
OWASP (Open Worldwide Application Security Project) — некоммерческая организация, публикующая открытые стандарты, руководства и инструменты по безопасности веб-приложений.
OWASP Top 10 — список десяти наиболее критичных угроз безопасности веб-приложений, обновляемый OWASP раз в несколько лет.
SAST (Static Application Security Testing) — статический анализ исходного кода приложения для поиска уязвимостей без его запуска (инструменты: Semgrep, Bandit, SonarQube).
DAST (Dynamic Application Security Testing) — динамический анализ безопасности работающего приложения путём отправки тестовых запросов; выявляет уязвимости, проявляющиеся только в режиме выполнения.
DOM (Document Object Model) — программный интерфейс браузера, представляющий структуру HTML-страницы в виде дерева объектов, которые можно читать и изменять через JavaScript.
CSRF (Cross-Site Request Forgery) — атака, при которой вредоносный сайт вынуждает браузер жертвы отправить запрос к другому приложению, используя сохранённые cookie.
mXSS (Mutation XSS) — класс XSS-атак, при котором браузер при парсинге HTML мутирует разметку таким образом, что payload, безопасный на входе в санитайзер, становится исполняемым после вставки в DOM.
SIEM (Security Information and Event Management) — система сбора, корреляции и анализа событий безопасности из различных источников в реальном времени для обнаружения инцидентов.
CVSS (Common Vulnerability Scoring System) — стандартизированная система числовой оценки серьёзности уязвимостей по шкале от 0 до 10.
Пентест (Penetration Testing) — контролируемая имитация атаки на систему, проводимая независимыми специалистами с целью выявления уязвимостей до того, как ими воспользуются реальные злоумышленники.
SQL-инъекция (CWE-89, «Improper Neutralization of Special Elements used in an SQL Command» по определению MITRE) — уязвимость, при которой приложение конструирует SQL-команду из данных пользователя без нейтрализации специальных символов, и атакующий меняет логику запроса. Канонический payload ' OR '1'='1, описанный в OWASP Web Security Testing Guide (раздел WSTG-INPV-05), превращает условие WHERE username='...' AND password='...' во всегда-истинное и обходит аутентификацию без знания пароля.
Такой пример выглядит наивно, но именно этот класс атак открыл Cl0p доступ к данным 93,3 млн человек через MOVEit Transfer.
OWASP Web Security Testing Guide (раздел WSTG-INPV-05) выделяет четыре класса:
Python с прямой конкатенацией строки: python
def login(username, password): def login(username, password): query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'" cursor.execute(query) return cursor.fetchone()
PHP с MySQL и параметром из GET-запроса: php
<?php $user = $_GET['user']; $query = "SELECT * FROM users WHERE username = '" . $user . "'"; $result = mysqli_query($conn, $query); ?>
Если в Python-форму передать username = "admin' --", итоговый запрос превратится в: sql
SELECT * FROM users WHERE username = 'admin' --' AND password = '...'
Двойной дефис закомментирует проверку пароля, и приложение вернёт пользователя admin. Та же логика работает в PHP-примере: ?user=admin' OR 1=1 -- выгрузит всю таблицу пользователей.
OWASP SQL Injection Prevention Cheat Sheet называет параметризованные запросы (prepared statements) «Defense Option 1» — обязательным методом для всех SQL-команд, в которые попадает пользовательский ввод. Драйвер БД отправляет шаблон запроса и значения параметров двумя отдельными сообщениями протокола (в PostgreSQL — Parse и Bind расширенного протокола). Значение никогда не интерпретируется СУБД как часть синтаксиса, поэтому payload ' OR '1'='1 остаётся строковым литералом, который попадает в WHERE username = ? целиком и не находит совпадения.
Python, psycopg2: python
cursor.execute( "SELECT id, email FROM users WHERE username = %s AND password_hash = %s", "SELECT id, email FROM users WHERE username = %s AND password_hash = %s", (username, password_hash) )
PHP, PDO с именованными параметрами: php
<?php $stmt = $pdo->prepare("SELECT id, email FROM users WHERE username = :user"); $stmt->execute(['user' => $username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); ?>
Java, JDBC с PreparedStatement: java
PreparedStatement ps = conn.prepareStatement( "SELECT id, email FROM users WHERE username = ?" ); ps.setString(1, username); ResultSet rs = ps.executeQuery();
JavaScript, Node.js с pg: javascript
const result = await client.query( 'SELECT id, email FROM users WHERE username = $1', [username] );
Динамические имена таблиц и колонок параметризовать нельзя — для них применяется whitelist-валидация (см. ниже).
ORM-библиотеки (Django ORM, SQLAlchemy, Hibernate, Eloquent, TypeORM) по умолчанию строят запросы через параметризацию. Код вида User.objects.filter(username=username) в Django безопасен, потому что ORM собирает prepared statement под капотом.
Опасные места — raw-запросы и сырой SQL: python
# Django: уязвимо, если username не очищен User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'") # Безопасно User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [username])
В SQLAlchemy опасен text() со строковой интерполяцией, в Eloquent — DB::raw() и whereRaw() без bindings. Любой raw-запрос — кандидат на ручной аудит и обязательная цель для SAST-сканеров (Semgrep, Bandit, SonarQube).
Валидация — фильтр на входе, но не замена параметризации. Whitelist-подход (разрешён только заранее известный набор символов) надёжнее blacklist. Для числового параметра — проверка типа и диапазона. Для перечисления (например, поле sort_by) — словарь допустимых значений: python
SORTABLE = {"created_at", "username", "email"} if sort_by not in SORTABLE: raise ValueError("invalid sort field") query = f"SELECT * FROM users ORDER BY {sort_by}"
Экранирование (mysqli_real_escape_string, addslashes) — резервный метод, к которому прибегают только при невозможности параметризации. OWASP SQL Injection Prevention Cheat Sheet квалифицирует его как «Defense Option 4» с явной пометкой «frail compared to other defenses». Классический обход — двухбайтовые символы кодировки GBK в связке PHP + MySQL: драйвер интерпретировал последовательность как «обратный слеш + кавычка», addslashes ломался, инъекция проходила. Проблема закрылась переходом на mysql_set_charset() и параметризованные PDO/mysqli.
Учётная запись приложения для БД — не root и не postgres. Минимальные права на нужные таблицы (SELECT, INSERT, UPDATE, DELETE) резко сужают ущерб при успешной инъекции: никаких DROP, CREATE, GRANT, доступа к information_schema, pg_catalog, mysql.user. Для разных модулей приложения лучше заводить разных пользователей: фронт читает витрину одним аккаунтом, бэкенд админки пишет другим.
Cross-Site Scripting (CWE-79, «Improper Neutralization of Input During Web Page Generation» по определению MITRE) — уязвимость, при которой приложение помещает данные, контролируемые пользователем, в веб-страницу без надлежащего экранирования, и браузер жертвы исполняет чужой JavaScript в контексте легитимного домена. CWE-79 возглавляет MITRE CWE Top 25 2025 года второй год подряд. Скрипт работает в контексте сессии: читает cookies без флага HttpOnly (RFC 6265, раздел 4.1.2.6), подменяет DOM, отправляет запросы с сессионными куками жертвы и выполняет действия от её имени.
В отчётах Positive Technologies XSS стабильно входит в топ-3 уязвимостей веб-приложений. По данным Solar 4RAYS за Q3 2025, XSS вышел на первое место с долей роста 37,7% к предыдущему кварталу. Уязвимости NetCat CMS, исправленные в 2025 году по находкам Positive Technologies, — это 23 CVE с CVSS 8,1–9,1, связка XSS и SQLi, затронувшая более 200 публично доступных инсталляций, 92,9% которых работали в России.
OWASP XSS Cheat Sheet и документация Mozilla Developer Network выделяют три класса.
JavaScript, который собирает приветствие из URL-параметра: javascript
const userInput = new URLSearchParams(location.search).get('name'); document.getElementById('greeting').innerHTML = 'Hello, ' + userInput;
Если жертва откроет /profile?name=, браузер выполнит скрипт и отправит cookies на сервер атакующего. Без флага HttpOnly это даст ему действующую сессию.
Аналогично уязвимый PHP-шаблон: php
<h1>Hello, <?= $_GET['name'] ?></h1>
И серверный Jinja2 при отключённом автоэкранировании или с фильтром |safe: text
<h1>Hello, {{ name|safe }}</h1>
Базовый принцип: данные пользователя экранируются в момент вставки в HTML, и метод кодирования зависит от контекста. В HTML-тексте <_script> превращается в <script>. В атрибуте — двойные кавычки в ". В JS-контексте — экранирование кавычек и обратного слеша. В URL — percent-encoding (encodeURIComponent).
Современные фреймворки (React, Vue, Angular, Jinja2, Twig, Razor) экранируют HTML-контекст автоматически. React-выражение <div_>{userInput}</div_> безопасно: библиотека вставит текст, а не парсит его как HTML. Опасные точки — dangerouslySetInnerHTML в React, v-html в Vue, фильтр |safe и Markup() в Jinja2, прямые innerHTML и document.write в ванильном JS.
Для каждого контекста — отдельная библиотека кодирования. Для Java стандарт — OWASP Java Encoder (Encode.forHtml, Encode.forJavaScript, Encode.forUriComponent). Для PHP — htmlspecialchars($s, ENT_QUOTES | ENT_HTML5, 'UTF-8'). Для Python — markupsafe.escape или html.escape. Универсального «escape для всего» не существует: HTML-кодирование внутри JS-блока не защищает, а JS-кодирование в HTML-атрибуте ломает разметку.
CSP — стандарт W3C (действующая версия — CSP Level 3, W3C Working Draft). Передаётся через HTTP-заголовок Content-Security-Policy и инструктирует браузер, какие источники скриптов, стилей, изображений и фреймов разрешены. При срабатывании политика блокирует загрузку и отправляет отчёт на указанный endpoint. Реальную защиту от XSS CSP даёт только в строгом режиме — без 'unsafe-inline' и 'unsafe-eval' в script-src.
Базовая политика для приложения с собственным фронтом и CDN: text
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; report-uri /csp-report
Inline-скрипты подписываются через nonce или hash: text
Content-Security-Policy: script-src 'self' 'nonce-rAnd0mBase64'
xml
<script nonce="rAnd0mBase64">/* инлайн-логика */</script>
На этапе внедрения политику катят в режиме Content-Security-Policy-Report-Only: браузер не блокирует, но шлёт отчёты. Через одну-две недели по логам собирают список легитимных источников и переключаются в боевой режим.
Флаги cookie сужают поверхность атаки даже в том случае, если XSS-вектор сработал: text
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
HttpOnly — JavaScript через document.cookie значение не прочитает. Secure — cookie передаётся только по HTTPS. SameSite=Strict или Lax — браузер не пошлёт cookie на cross-site запрос, что блокирует CSRF и часть сценариев, где XSS на стороннем домене бьёт по основному приложению.
Все сессионные cookies — всегда с тройкой флагов. Для CSRF-токенов SameSite=Lax допустимо там, где Strict ломает UX при переходах по ссылкам.
В сценариях, где пользователь должен вводить HTML (WYSIWYG-редактор, комментарии с форматированием), output encoding не подходит: текст должен остаться HTML. Здесь применяют санитайзеры, которые разбирают разметку и оставляют только разрешённый whitelist тегов и атрибутов.
JavaScript — DOMPurify: javascript
import DOMPurify from 'dompurify'; const clean = DOMPurify.sanitize(userHtml, { ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'], ALLOWED_ATTR: ['href', 'title'] }); element.innerHTML = clean;
Python — bleach: python
import bleach clean = bleach.clean( user_html, tags=['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'], attributes={'a': ['href', 'title']}, protocols=['http', 'https', 'mailto'] )
Для PHP — HTML Purifier, для Java — OWASP Java HTML Sanitizer. Самописная регулярка для фильтрации тегов — плохая идея: документация DOMPurify приводит десятки обходов через мутации DOM, SVG-namespace и mXSS.
WAF (Web Application Firewall) терминирует HTTPS-сессию и разбирает HTTP-запросы до того, как они доходят до приложения: тело, заголовки, query-параметры, cookies. Он сверяет содержимое с базой сигнатур, регулярных выражений и эвристик — и решает: пропустить запрос, заблокировать или пометить.
Открытый стандарт правил — ModSecurity Core Rule Set (OWASP CRS). В CRS 49 из 249 правил отвечают за SQL-инъекции (группа CRS-942), отдельная группа CRS-941 закрывает XSS. Правила ловят базовые payload'ы (UNION SELECT, ' OR 1=1, <script>, javascript: в атрибутах), детектируют обфускацию через комментарии и кодировки, считают оценку аномальности запроса и блокируют при превышении порога.
WAF — не замена безопасному коду: параметризованный запрос защищает от SQLi даже при отключённом WAF, а исправно настроенный CSP режет XSS, который WAF пропустил. Основная ценность WAF в другом — он снимает массовый трафик автоматизированных сканеров, тормозит эксплуатацию 0-day и закрывает окно между обнаружением уязвимости и выкаткой патча. На фоне ~48 000 CVE, опубликованных в 2025 году, это окно регулярно растягивается на недели.
В WAF VK Cloud базовый набор правил для SQLi и XSS включён по умолчанию. Чувствительность конфигурируется: строгая политика для production с блокировкой по первой сработке, мягкая для staging — режим detection-only с записью в лог. Логи запросов, причин блокировки и оценок аномальности уходят в S3 или SIEM для разбора инцидентов и тонкой настройки правил под легитимный трафик приложения.
SQL-инъекции и XSS — уязвимости с многолетней историей, но именно они держат верхние строчки CWE Top 25 и продолжают приводить к утечкам данных миллионов пользователей. Полноценная защита строится в три слоя: безопасный код (parameterized queries, output encoding, sanitization), HTTP-заголовки (CSP, HttpOnly/Secure/SameSite) и WAF как сетевой фильтр перед приложением. Один слой не заменяет другой — каждый закрывает свой класс ошибок.
Чек-лист выше — минимальная база для аудита работающего сервиса. А WAF VK Cloud закрывает сетевой уровень: правила SQLi и XSS из CRS, тонкая настройка чувствительности и логирование в SIEM подключаются без переписывания приложения.
Наши специалисты свяжутся с вами в ближайшее время и ответят на все вопросы.

Будем держать в курсе новостей и облачных трендов




