VK Cloud

SQL-инъекции и XSS-атаки: как защитить веб-приложение от самых частых угроз

30 июня 2026 г.
шпрингер.png
Елена Шпрингер
Автор статьи
_blog_head_42.png

Атака на 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, покажем уязвимый и защищённый код, и сведём защиту в чек-лист, который можно прогнать по продакшену уже сегодня.

погоржельский2.jpg

Статья подготовлена вместе с экспертом

Станислав Погоржельский, технологический евангелист 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-инъекции: как работает атака

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) выделяет четыре класса:

  • Classic (in-band) — результат запроса возвращается прямо в HTTP-ответе. Атакующий видит данные напрямую: дамп таблицы пользователей, хеши паролей, токены.
  • Union-based — частный случай in-band. Через UNION SELECT атакующий присоединяет к легитимному запросу выборку из других таблиц, в том числе системных (information_schema.tables, pg_catalog).
  • Blind — приложение не возвращает данные, но реагирует на запрос разным поведением (например, 200 OK против 500). Атакующий восстанавливает данные побайтово через булевы условия: AND SUBSTRING(password,1,1)='a'.
  • Time-based — разновидность blind, где сигналом служит задержка ответа: AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0). Медленнее, но работает там, где разница в ответах не видна.

Пример уязвимого кода

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 -- выгрузит всю таблицу пользователей.

Защита от SQL-инъекций

Parameterized queries и prepared statements

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: автоматическая защита

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. Для разных модулей приложения лучше заводить разных пользователей: фронт читает витрину одним аккаунтом, бэкенд админки пишет другим.

XSS-атаки: как работает атака

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% которых работали в России.

Типы XSS

OWASP XSS Cheat Sheet и документация Mozilla Developer Network выделяют три класса.

  • Stored (persistent) — payload сохраняется в БД и подгружается всем, кто открывает страницу. Классический сценарий: комментарий с тегом на форуме, аватарка с onerror-обработчиком, имя в профиле. -Reflected — payload приходит в URL и отражается в HTML без сохранения. Атакующий рассылает ссылку: https://app.example.com/search?q=. Защита начинается с правила: параметры URL не попадают в HTML без кодирования.
  • DOM-based — уязвимость в клиентском JavaScript. Сервер чист, но скрипт читает location.hash или document.referrer и вставляет значение в innerHTML. Такой сценарий не виден на бэкенде и не ловится серверными WAF без JS-инспекции. Для этого класса существует отдельный документ — OWASP DOM-based XSS Prevention Cheat Sheet.

Пример уязвимого кода

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>

Защита от XSS

Output encoding: контекстно-зависимое экранирование

Базовый принцип: данные пользователя экранируются в момент вставки в 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-атрибуте ломает разметку.

Content Security Policy (CSP)

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: браузер не блокирует, но шлёт отчёты. Через одну-две недели по логам собирают список легитимных источников и переключаются в боевой режим.

HttpOnly и Secure cookies

Флаги 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 при переходах по ссылкам.

Sanitization-библиотеки

В сценариях, где пользователь должен вводить 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 как дополнительный уровень защиты

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-запросы — через parameterized queries или ORM. Raw-SQL отдельно помечен в коде и проходит ручной review при каждом изменении.
  • Output encoding во всех контекстах: HTML, HTML-атрибут, JS, URL, CSS — каждый со своим методом кодирования, никаких dangerouslySetInnerHTML и |safe без санитайзера.
  • CSP-заголовок настроен без 'unsafe-inline' и 'unsafe-eval' для script-src, inline-скрипты через nonce, политика обкатана через Report-Only.
  • Cookies с тройкой флагов: HttpOnly, Secure, SameSite=Strict (или Lax там, где Strict ломает UX) для всех сессионных и аутентификационных cookies.
  • Input validation по whitelist: тип, длина, формат, перечисление допустимых значений; blacklist-фильтры — не основная защита.
  • БД-пользователь с минимальными правами, отдельные учётные записи для разных модулей, никаких DROP/GRANT/доступа к системным каталогам.
  • WAF подключён и настроен: базовый CRS-набор для SQLi и XSS включён, политика откалибрована под легитимный трафик, логирование в SIEM.
  • Зависимости обновлены — регулярный прогон npm audit, pip-audit, composer audit, OWASP Dependency-Check, критические CVE закрываются в рамках SLA.
  • Security-тесты в CI/CD: SAST (Semgrep, Bandit, SonarQube), SCA, базовый DAST на staging; pipeline падает на high-severity находках.
  • Пентест минимум раз в год: внешний независимый аудит для критичных сервисов, результаты — в backlog с приоритетом по CVSS.

Заключение

SQL-инъекции и XSS — уязвимости с многолетней историей, но именно они держат верхние строчки CWE Top 25 и продолжают приводить к утечкам данных миллионов пользователей. Полноценная защита строится в три слоя: безопасный код (parameterized queries, output encoding, sanitization), HTTP-заголовки (CSP, HttpOnly/Secure/SameSite) и WAF как сетевой фильтр перед приложением. Один слой не заменяет другой — каждый закрывает свой класс ошибок.

Чек-лист выше — минимальная база для аудита работающего сервиса. А WAF VK Cloud закрывает сетевой уровень: правила SQLi и XSS из CRS, тонкая настройка чувствительности и логирование в SIEM подключаются без переписывания приложения.

Оставьте заявку, чтобы получить консультацию

Наши специалисты свяжутся с вами в ближайшее время и ответят на все вопросы.

section-subscribe_2x.png

            Узнавайте о выходе новых статей в блоге первыми!

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

            section-subscribe_2x.png
              section-subscribe_2x.png
              Теги: SQL, Python
              Ссылка скопирована
              Поделиться

              Почитать по теме

              _blog_head_45.png
              24 июня

              OWASP Top 10: главные уязвимости веб-приложений и как от них защититься

              _blog_head_126.png
              4 июня

              DDoS-атаки в 2026 году: какие бывают, чем опасны и как от них защититься

              40+ готовых сервисов