Атаки и их предотвращение, которые должен знать каждый веб-разработчик

Безопасность почти всегда ломается в стыке: «валидация была», но не там; «авторизация есть», но не на одном эндпоинте; «данные экранируются», но не в одном месте в шаблоне. Хорошая новость: большая часть реальных инцидентов укладывается в повторяющиеся классы ошибок — их можно закрывать системно.

Связанные материалы: Наблюдаемость и мониторинг · БД под нагрузкой

Содержание


Модель угроз: что защищаем и от кого

Перед техникой — короткая рамка:

  • Атакующий: анонимный пользователь, пользователь с аккаунтом, партнёр с API-ключом, «свой» в VPN, злоумышленник с доступом к CI/CD.
  • Активы: деньги, персональные данные, аккаунты, админка, интеграции, секреты, доступ к внутренней сети.
  • Поверхность атаки: формы и API, редиректы, webhooks, импорты/экспорты, загрузки файлов, интеграции (S3, SMTP, платежи).

Практическое правило: любой ввод недоверенный (включая заголовки, cookies, webhook payload, параметры URL, JSON из очереди).


Инъекции: SQLi, command injection, template injection

SQL Injection (SQLi)

SQLi почти всегда начинается с «всего один сырой кусок SQL». Защита — не «экранировать», а параметризовать.

Плохо (конкатенация):

$rows = DB::select("SELECT * FROM users WHERE email = '{$email}'");

Лучше (плейсхолдеры):

$rows = DB::select('SELECT * FROM users WHERE email = ?', [$email]);

Ещё лучше — держать запросы в Eloquent/Query Builder там, где это возможно, и не хранить «режимы» числом (см. также заметки про PDO в гайдах по версиям PHP).

Отдельный класс проблем — динамические имена колонок / сортировка. Параметры не подставляют идентификаторы, поэтому делайте allowlist:

$allowed = ['created_at', 'email', 'id'];
$sort = in_array($request->get('sort'), $allowed, true) ? $request->get('sort') : 'created_at';

$users = User::query()->orderBy($sort, 'desc')->paginate();

Command injection

Если приложение вызывает shell, защита — не пропускать пользовательский ввод в командную строку. Даже escapeshellarg() — не серебряная пуля, а последний барьер.

Если нужно конвертировать файл/генерировать превью:

  • предпочитайте библиотеку, а не exec();
  • если всё же CLI — фиксируйте бинарник, аргументы и директории, запускайте в ограниченном окружении (контейнер/очередь/worker под отдельным пользователем).

Template injection

Самый опасный вариант — когда вы позволяете пользователю задавать «шаблон», который потом исполняется как Blade/Twig/Smarty.

Правило: пользовательский шаблон — только как данные, максимум через ограниченный, не Turing-complete формат (например, Markdown с жёстко заданным набором разрешённых тегов и безопасным рендером).


XSS: отражённый, хранимый, DOM-based

XSS — это выполнение JS в контексте вашего домена. Типовые источники:

  • вывод «как есть» ({!! ... !!} в Blade, innerHTML во фронте);
  • HTML в профиле/комментариях без санитайза;
  • вставка данных в <script>/атрибуты без правильного экранирования.

Базовые меры

  • Экранирование по умолчанию: в Blade используйте {{ $value }}. Не используйте raw-вывод без жёсткого основания.
  • Разделяйте контексты: HTML, атрибуты, JS-строки, URL — разные правила экранирования.
  • Санитайз HTML**-ввода**: если вы разрешаете «форматирование», применяйте allowlist (теги/атрибуты) и вырезайте on*-обработчики, javascript:-URL и т.п.
  • CSP (Content Security Policy): это не исправляет логику, но резко снижает ущерб от XSS.

Пример CSP-стратегии для старта (идеально — с nonces и без 'unsafe-inline'):

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

CSRF: почему «я же GET» не спасает

CSRF — когда браузер пользователя «сам» отправляет запрос на ваш сайт с его cookies/сессией, потому что это делает другой сайт (формой, изображением, JS).

Меры защиты

  • CSRF-токены на state-changing запросах (POST/PUT/PATCH/DELETE) — Laravel делает это из коробки.
  • SameSite cookies: минимум Lax, для чувствительных сценариев — Strict (с пониманием UX), для кросс-сайтовых кейсов — None; Secure.
  • Проверка Origin/Referer на особо критичных действиях (смена email, вывод средств).
  • Не делать изменения состояния через GET вообще.

Аутентификация и сессии: кража токенов, фиксация, куки

Что обычно ломают:

  • слабые пароли + отсутствие rate limiting → credential stuffing;
  • сессии: XSS украл токен/куки; фиксация сессии; токен не инвалидируется;
  • долгоживущие токены без ротации и привязки к устройству.

Минимальный набор:

  • Rate limit на логин/OTP/восстановление пароля.
  • Хэширование паролей только password_hash() (bcrypt/argon2), без «своих» криптосхем.
  • Cookies для сессии: HttpOnly, Secure, SameSite, короткая жизнь там, где можно.
  • Ротация session ID после логина и повышения прав (Laravel умеет regenerate()).

Контроль доступа и IDOR: «это же просто id»

IDOR (Insecure Direct Object Reference) — когда пользователь угадывает/перебирает идентификаторы и видит/меняет чужие ресурсы.

Типичные причины:

  • проверка «пользователь залогинен» вместо проверки владения/ролей;
  • авторизация есть в UI, но нет на API;
  • «админский» параметр в запросе управляет поведением.

Защита

  • Политики/гейты на каждый ресурс и каждое действие (view/update/delete).
  • Мульти-тенантность: всегда фильтруйте по tenant_id/account_id на уровне запросов.
  • Не полагайтесь на скрытие (кнопки/роуты) как на безопасность.
  • Логи доступа к чувствительным объектам (особенно чтение PII).

Пример (идея): вместо User::findOrFail($id)auth()->user()->orders()->findOrFail($id).


Загрузки файлов и путь: upload, path traversal, RCE рядом

Загрузки — любимое место для RCE «рядом с» приложением.

Что проверять

  • Содержимое, а не только расширение. MIME от браузера — недоверенный.
  • Ограничение размера и количества файлов.
  • Хранение вне web-root, раздача через контроллер или отдельный домен/CDN.
  • Случайные имена (не использовать исходное имя как путь).
  • Запрет исполнения в директории upload (на уровне веб-сервера).

Laravel-паттерн (идея): Storage::disk('private')->putFile(...) и выдача через signed URL/контроллер.

Path traversal чаще всего появляется в «скачай файл по имени»:

  • запрещайте ../, \, нулевые байты;
  • используйте allowlist реальных файлов/идентификаторов, а не путь из запроса.

SSRF: когда сервер ходит «куда не надо»

SSRF возникает, когда вы берёте URL от пользователя и делаете запрос от имени сервера (импорт аватарки, webhook tester, «проверить ссылку», PDF-сервис).

Опасность: доступ к внутренним адресам (metadata endpoints облака, Redis/Consul, админки), обход сетевых границ.

Защита

  • Allowlist доменов/хостов, а не blacklist.
  • Запрет внутренних IP-диапазонов (127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16, 172.16/12, 192.168/16, ::1, fc00::/7).
  • Отключение редиректов или проверка цепочек редиректов.
  • Таймауты, лимиты размера ответа, ограничение протоколов (только https).

Небезопасная десериализация и подмена объектов

Если вы десериализуете данные, которые может подделать атакующий (cookies, hidden fields, payload из внешней очереди, кэш без подписи), вы рискуете:

  • подменой полей/ролей;
  • gadget chain и RCE (в экосистемах, где это возможно).

Правило: не хранить сериализованные объекты в недоверенных местах. Для токенов — подпись (HMAC), для сложных данных — JSON + строгая схема + серверная валидация.


Браузерные защиты: clickjacking, CORS, заголовки

Clickjacking

Закрывайте встраивание в iframe:

X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'

CORS

Суть: CORS — не «защита от запросов», а политика чтения ответа браузером. Ошибки:

  • Access-Control-Allow-Origin: * вместе с cookies/credentials;
  • разрешение лишних методов/заголовков;
  • доверие к Origin без строгого списка.

Базовые security headers

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()

DoS и abuse: rate limit, брутфорс, дорогостоящие операции

Самые частые «полу-DoS» в бизнес-приложениях — это не гигабиты трафика, а дешёвые для атакующего, дорогие для вас запросы:

  • поиск без индексов, экспорт больших отчётов, генерация PDF;
  • endpoints без rate limit (логин, отправка email/OTP, «проверка промокода»);
  • сложная валидация/регулярки на больших строках.

Меры:

  • rate limiting по IP + по аккаунту + по ключу (в зависимости от сценария);
  • очереди на дорогие операции;
  • таймауты, лимиты размеров, ограничения глубины JSON;
  • кэширование «публичных» тяжёлых ответов (с учётом персонализации!).

Чеклист для ревью и релиза

Ввод и данные

  • [ ] Валидация входа на границе (формы/API/webhooks), нормализация типов.
  • [ ] Allowlist для сортировки/фильтров/полей, которые становятся идентификаторами в запросах.
  • [ ] Нет state-change через GET.

Рендер и браузер

  • [ ] Экранирование по умолчанию, нет необоснованного raw HTML.
  • [ ] CSP/frame-ancestors, защита от clickjacking.
  • [ ] HSTS, nosniff, разумный Referrer-Policy.

Доступ

  • [ ] Авторизация на сервере для каждого действия (policy/gate), не только «в UI».
  • [ ] Нет IDOR: выборки ограничены владельцем/тенантом.
  • [ ] Логи аудита для чувствительных операций.

Интеграции

  • [ ] SSRF: allowlist доменов, запрет внутренних IP, лимиты и таймауты.
  • [ ] Webhooks: подпись, защита от повторов (replay), идемпотентность.

Uploads

  • [ ] Проверка типа/размера, хранение вне web-root, запрет исполнения.
  • [ ] Случайные имена, нет путей из пользовательского ввода.

Итог

Если нужно запомнить один принцип: безопасность — это инварианты на границе. Ввод проверяется один раз и правильно; вывод экранируется по умолчанию; доступ всегда проверяется сервером; интеграции «снаружи» подписаны; всё, что дорого, ограничено по частоте и вынесено из критического пути.