Атаки та їх запобігання, які має знати кожен веброзробник

Безпека майже завжди ламається на стиках: валідація є, але не на межі; авторизація реалізована, але один ендпойнт її пропустив; екранування працює, але в одному шаблоні вивели raw HTML. Добра новина: більшість реальних інцидентів — це повторювані класи помилок, які можна закривати системно.

Пов’язані матеріали: Спостережуваність і моніторинг · Бази даних під навантаженням

Зміст


Модель загроз: що захищаємо і від кого

Перед технікою — коротка рамка:

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

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


Ін’єкції: 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]);

Окремий клас проблем — динамічні імена колонок / сортування. Параметри не підставляють ідентифікатори, тому робіть 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() — це крайній бар’єр, а не дизайн.

Якщо потрібно запускати CLI (конвертація файлів, прев’ю):

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

Template injection

Найнебезпечніший варіант — коли користувач задає «шаблон», який потім виконується як Blade/Twig/Smarty.

Правило: користувацький шаблон — лише як дані, максимум через обмежений формат (наприклад, Markdown з allowlist і безпечним рендером).


XSS: відбитий, збережений, DOM-based

XSS — це виконання JS у контексті вашого домену. Типові джерела:

  • raw-рендер ({!! ... !!} у Blade, innerHTML на фронті);
  • HTML у профілі/коментарях без санітизації;
  • вставка даних у <script>/атрибути без правильного кодування.

Базові захисти

  • Екранування за замовчуванням: у Blade використовуйте {{ $value }}.
  • Розділяйте контексти: HTML, атрибути, JS-рядки, URL — різні правила.
  • Санітизувати HTML-ввід allowlist’ом (теги/атрибути) та прибирати on*, javascript: тощо.
  • CSP як обмежувач шкоди.

Стартовий 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; для кроссайтових — None; Secure.
  • Перевірка Origin/Referer для критичних дій (зміна email, виплати).
  • Не міняти стан через GET.

Автентифікація та сесії: крадіжка токенів, фіксація, cookies

Що найчастіше ламають:

  • слабкі паролі + відсутній rate limiting → credential stuffing;
  • крадіжка сесії через XSS; фіксація сесії; відсутня інвалідація;
  • довгоживучі токени без ротації та прив’язки до пристрою.

Базовий набір:

  • Rate limit на логін/OTP/скидання пароля.
  • Хешування паролів через password_hash() (bcrypt/argon2).
  • Cookies сесії: HttpOnly, Secure, SameSite, по можливості короткий TTL.
  • Ротація session ID після логіна та підвищення прав.

Контроль доступу й IDOR: «це ж просто id»

IDOR — коли користувач підбирає/перебирає ідентифікатори й читає або змінює чужі ресурси.

Захист:

  • Policies/gates на кожен ресурс і дію.
  • У multi-tenant — фільтрація за tenant_id/account_id на рівні запитів.
  • Не покладатися на «сховати кнопку» як на безпеку.
  • Аудит-логи для чутливих дій.

Ідея: замість Order::findOrFail($id)auth()->user()->orders()->findOrFail($id).


Завантаження файлів і шлях: upload, path traversal, RCE поруч

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

  • перевіряйте вміст, а не лише розширення; MIME від браузера — недовірений;
  • ліміти розміру/кількості;
  • зберігання поза web-root, роздача через контролер або окремий домен/CDN;
  • випадкові імена файлів; не використовувати оригінальне ім’я як шлях;
  • заборонити виконання в директорії upload на рівні вебсервера.

Path traversal часто з’являється в «скачай файл за ім’ям»: відкидайте ../, \, нуль-байти й використовуйте allowlist реальних ID.


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

SSRF виникає, коли ви берете URL від користувача і робите запит від імені сервера (імпорт аватарки, link preview, webhook tester).

Захист:

  • allowlist доменів/хостів;
  • блок приватних 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.

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


Браузерні захисти: clickjacking, CORS, заголовки

Clickjacking:

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

CORS — це політика читання відповіді в браузері. Уникайте * разом із credentials і тримайте строгий allowlist Origin.

Базові заголовки:

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, брутфорс, дорогі операції

Найчастіше ламають «дешево для атакера — дорого для вас»: пошук без індексів, експорти, PDF, логін/OTP без rate limit.

Міри:

  • rate limiting по IP + акаунту + ключу;
  • черги для дорогих операцій;
  • таймаути й ліміти розміру/глибини JSON;
  • кешування важких публічних відповідей (з урахуванням персоналізації).

Чеклист для рев’ю та релізу

  • [ ] Валідація вводу на межі (форми/API/webhooks), нормалізація типів.
  • [ ] Allowlist для сортувань/фільтрів/ідентифікаторів.
  • [ ] Екранування за замовчуванням, немає необґрунтованого raw HTML.
  • [ ] Авторизація на сервері для кожної дії; немає IDOR.
  • [ ] SSRF-захисти (allowlist, приватні IP, таймаути).
  • [ ] Uploads: перевірки, зберігання поза web-root, заборона виконання.

Підсумок

Ключовий принцип: безпека — це інваріанти на межі. Один раз правильно перевіряйте ввід, екрануйте вивід за замовчуванням, перевіряйте доступ на сервері, підписуйте зовнішні інтеграції та обмежуйте частоту всього дорогого.