rzaripov.kz

Как я защищаю API на PHP от атак CSRF, XSS и SQL-инъекций

Я Равиль Зарипов, разработчик из Алматы. В проектах для Android, iOS, Windows, macOS и Linux я использую разные технологии, включая PHP для серверной части. Защита API для меня не сводится к одной проверке: каждый слой должен ограничивать возможности злоумышленника подделать запрос, прочитать чужие данные или изменить состояние системы.

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

У трёх основных классов атак разные точки входа: CSRF злоупотребляет автоматической отправкой куки браузером, XSS использует неверный контекст вывода, а SQL-инъекция меняет структуру запроса. Сравнение показывает, почему универсального фильтра недостаточно: мера должна соответствовать месту возникновения опасности и дополняться независимой проверкой на другом уровне.

Угроза Как используется атака Основная мера Независимый контроль
CSRF Чужая страница заставляет браузер выполнить изменяющий запрос Сессионный CSRF-токен и проверка источника SameSite, Secure, HttpOnly, ограничение CORS
XSS Сохранённое значение исполняется как код в HTML, SVG или DOM Экранирование по контексту и безопасный вывод на клиенте JSON, CSP, очистка разрешённого HTML
SQL-инъекция Пользовательский ввод изменяет синтаксис SQL Параметризованные запросы PDO Белый список полей, минимальные права базы данных

Модель доверия и безопасные границы API

Сначала я разделяю аутентификацию и авторизацию. Токен или сессия подтверждают, кто отправил запрос, но сервер отдельно проверяет, разрешено ли этому пользователю действие с конкретным объектом. Для JWT я проверяю подпись, срок действия, аудиторию и издателя, а для каждого запроса учитываю область полномочий. Проверка одной лишь подписи не даёт доступа к чужому ресурсу.

Входные данные считаю недоверенными независимо от клиента. Тип, длину, диапазон, допустимый перечень и формат проверяю по схеме, а неизвестные поля отклоняю там, где это не ломает совместимость. Идентификаторы привязываю к текущему пользователю и контексту, чтобы нельзя было заменить чужой идентификатор в URL или теле запроса. Файлы проверяю по MIME-типу, размеру и содержимому, а не по одному расширению.

Транспорт и ответ также входят в модель защиты. API работает только через HTTPS, не возвращает стек вызовов, внутренние пути, SQL-запросы и секреты. Для JSON устанавливаю Content-Type: application/json, добавляю X-Content-Type-Options: nosniff, запрещая браузеру угадывать тип содержимого. В тело ответа помещаю безопасный код ошибки и идентификатор обращения для сопоставления с журналом.

CSRF: токены, куки и проверка источника

Межсайтовая подделка запроса опасна, когда API меняет состояние и доверяет сессионным куки, которые браузер прикрепляет автоматически. Страница злоумышленника способна инициировать POST от имени вошедшего пользователя. Если токен хранится в заголовке Authorization и клиент не подставляет его автоматически, браузерный сценарий обычно не воспроизводится; куки-аутентификацию всё равно необходимо защищать.

Для сессионной аутентификации я использую CSRF-токен, связанный с сессией и создаваемый криптографически стойким генератором. Клиент получает токен через защищённый ответ и возвращает его в отдельном заголовке, а сервер сравнивает значения через hash_equals. Токен не должен попадать в URL, журнал или доступный скриптам куки. При выдаче нового идентификатора сессии прежний токен также заменяется.

Дополнительно ставлю SameSite=Lax или Strict для сессионной куки, добавляю Secure и HttpOnly, а операции изменения принимаю только через POST, PUT, PATCH и DELETE. Проверка Origin по списку разрешённых источников, а при его отсутствии Referer, служит вторым барьером. CORS не считаю защитой CSRF: он управляет чтением ответа браузером, но простая HTML-форма может отправить запрос без разрешения на чтение результата.

XSS: кодирование, JSON и безопасный клиент

Межсайтовый скриптинг возникает, когда данные считаются кодом и вставляются в опасный контекст. Для JSON API риск снижает отсутствие HTML-вывода, но он не исчезает: значение может сохраниться в базе, попасть в веб-интерфейс мобильного приложения, панель администратора или SVG. Экранирование на входе не универсально, поскольку правила зависят от места отображения.

Обычные пользовательские строки при вставке в HTML кодирую через htmlspecialchars с UTF-8 и ENT_QUOTES. В JSON применяю json_encode с флагами JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS и JSON_HEX_QUOT, если ответ может быть встроен в HTML-контекст. Отдельно запрещаю возвращать сырые фрагменты ошибок и пользовательский HTML в поле, предназначенное для текста. Сложное форматирование проходит через безопасный фильтр с белым списком разрешённых тегов и атрибутов.

На клиенте текст вывожу через текстовые узлы или безопасные API, избегая innerHTML, dangerouslySetInnerHTML и аналогичных конструкций для непроверенных данных. Заголовок Content-Security-Policy ограничивает источники исполняемых скриптов и служит защитой в глубину. Особое внимание уделяю сохранённым данным: опасная строка может пройти валидацию формы и активироваться спустя время в панели администратора или другом клиенте.

SQL-инъекции: параметризация и доступ к базе

SQL-инъекция возникает, когда в строку запроса вставляются данные и получают возможность изменить его структуру. В PHP я использую prepared statements PDO: значения передаю отдельно через ? или именованные placeholders, а не склеиваю с SQL. Такой запрос сохраняет синтаксическую границу между кодом и данными, поэтому кавычки в имени пользователя остаются частью строки.

Параметрами можно передать значения, но не имена таблиц и столбцов. Для динамических идентификаторов создаю строгий список допустимых вариантов и выбираю из него по логическому имени параметра. Пользовательский текст в ORDER BY или имени поля не передаю. Сортировку по белому списку, пагинацию и фильтры строю только из заранее определённых полей и направлений.

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

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

Проверки, наблюдаемость и поддержка

Безопасность поддерживаю как часть повторяемой разработки. В PHP-проекте фиксирую версии зависимостей, запускаю composer audit, статический анализ PHPStan или Psalm, линтеры конфигурации и проверку секретов. Конфигурацию анализаторов и шаблоны проверок храню в репозитории, чтобы они одинаково работали у всей команды. Известные CVE для используемых библиотек отслеживаю вместе с выпусками приложений на разных платформах.

Помимо успешных сценариев, автотесты покрывают негативные ситуации. В CI добавляю запросы без CSRF-токена и с неверным Origin, значения с XSS-последовательностями и строки, характерные для SQL-инъекций. Проверяю коды ошибок, заголовки, отсутствие секретов и неизменность чужих данных. Тесты выполняю на отдельной базе с подготовленными записями, чтобы проверка не зависела от случайного состояния.

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