Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Middle: питання на співбесіді з теми «Ін'єкції й XSS»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

CSRF (Cross-Site Request Forgery) - чужий сайт змушує браузер користувача надіслати запит на ваш сайт. Браузер автоматично додає cookie вашого сайту, тож запит виглядає як справжній запит залогіненого користувача.

<!-- на evil.example -->
<form action="https://bank.example/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>

Нападник не бачить відповіді, але дія виконується.

Захист 1 - CSRF-токен. Сервер кладе в сесію випадковий токен і вимагає його в кожному запиті, що змінює стан. Чужий сайт не може прочитати токен (same-origin policy), тож не може його підставити.

<form method="POST" action="/transfer">
    @csrf
</form>

Laravel перевіряє токен автоматично для всіх POST/PUT/PATCH/DELETE у групі web.

Захист 2 - атрибут cookie SameSite:

  • Lax (його ставить Laravel; браузери на Chromium застосовують його й до cookie без атрибута) - cookie не надсилається в міжсайтових POST-запитах, лише при звичайних переходах за посиланнями (GET).
  • Strict - не надсилається в жодних міжсайтових запитах.

Чому потрібні обидва: SameSite не захищає від запитів з піддоменів того самого сайту, а старі браузери його не підтримують. Токен - основний захист, SameSite - додатковий рівень.

Важливо: GET-запити не мають змінювати стан. GET /logout чи GET /delete?id=5 легко викликати картинкою на чужій сторінці.

API з токенами в заголовку Authorization (не в cookie) до CSRF не вразливі: браузер не додає такий заголовок автоматично.

Докладніше в документації: OWASP: запобігання CSRF

SSRF (Server-Side Request Forgery) - нападник змушує ваш сервер зробити запит туди, куди йому самому не дістатися: до внутрішніх сервісів, адмінок без авторизації, метаданих хмари.

Типові функції-мішені: імпорт аватара за URL, попередній перегляд посилання, вебхуки з адресою від користувача, конвертація HTML у PDF.

// Вразливо: адресу задає користувач
$image = Http::get($request->input('avatar_url'))->body();
// avatar_url = http://169.254.169.254/latest/meta-data/iam/security-credentials/
// → сервер віддасть тимчасові ключі хмари

Інші цілі: http://localhost:6379 (Redis без пароля), http://10.0.0.5:8080/admin, file:///etc/passwd у бібліотеках, що підтримують інші схеми.

Захист:

  • Білий список, якщо можливо: дозволені лише конкретні домени.
  • Лише http/https, жодних file:, gopher:, ftp:.
  • Перевірка IP після резолву DNS: відхиляти приватні діапазони (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), 127.0.0.0/8, 169.254.0.0/16, IPv6-аналоги. Перевіряти саме той IP, до якого піде з'єднання, - інакше обхід через DNS rebinding (домен спершу резолвиться в публічну адресу, а при з'єднанні - у внутрішню).
  • Редиректи: вимкнути або перевіряти кожен, бо https://evil.example може перенаправити на http://127.0.0.1.
  • Мережева ізоляція: вихідні запити через окремий проксі чи сервіс без доступу до внутрішньої мережі.
  • У хмарі - IMDSv2 на AWS (вимагає токен, який SSRF зазвичай не отримає).

Таймаути й ліміт розміру відповіді - теж обов'язкові, інакше функцію можна використати для навантаження на сервер.

Докладніше в документації: OWASP: запобігання SSRF

SSTI (server-side template injection) - дані користувача потрапляють у сам шаблон, а не в змінну, яку шаблон виводить. Рушій шаблонів компілює їх як код.

// вразливо: текст від користувача стає шаблоном
return Blade::render($request->input('greeting'), ['user' => $user]);
// greeting = "{{ system('cat .env') }}"  - або @php ... @endphp

Blade компілюється в PHP, тож SSTI в Blade - це виконання довільного PHP-коду на сервері.

Різниця, яку треба розуміти:

// безпечно: шаблон - ваш, дані - змінна, екранована {{ }}
return view('emails.welcome', ['greeting' => $request->input('greeting')]);

// небезпечно: дані користувача - частина шаблону
Blade::render($userTemplate, $data);

Де таке трапляється на практиці:

  • «шаблони листів, які редагує адміністратор» в адмінці - якщо адмінку зламано чи редакторів багато, це шлях до виконання коду на сервері;
  • конструктори сторінок і повідомлень для клієнтів (SaaS: «налаштуйте текст сповіщення з плейсхолдерами»);
  • генерація PDF чи документів з шаблонів, що зберігаються в базі;
  • Twig/Smarty/Mustache у сторонніх пакетах - кожен рушій має свої способи «вирватися» з пісочниці.

Як робити безпечно:

  • користувацьким шаблонам - лише заміна плейсхолдерів, а не повноцінний рушій:
$text = strtr($template, [
    '{name}' => e($user->name),
    '{order}' => e($order->number),
]);
  • якщо потрібна логіка (умови, цикли) - рушій з пісочницею і білим списком дозволених функцій (наприклад, Twig Sandbox), обмежений набір змінних, без доступу до об'єктів з методами;
  • Markdown замість HTML/Blade для контенту від користувачів - і рендер із санітизацією;
  • ніколи Blade::render, eval, create_function-подібних механізмів з даними ззовні.

Схоже на SSTI на клієнті: вираз {{ }} у даних, які Vue чи Angular компілюють як шаблон на сторінці (Vue змонтований на розмітку з користувацьким вмістом), - client-side template injection, що дає XSS.

Докладніше в документації: PortSwigger: Server-side template injection

XXE (XML External Entity) - атака через XML з визначенням зовнішньої сутності, яка вказує на файл чи URL. Парсер, що підставляє такі сутності, вставляє в документ вміст файлу сервера чи відповідь внутрішнього сервісу.

<?xml version="1.0"?>
<!DOCTYPE order [
  <!ENTITY secret SYSTEM "file:///var/www/app/.env">
]>
<order><comment>&secret;</comment></order>

Якщо застосунок потім показує чи зберігає comment - зловмисник отримує вміст .env. Варіанти - SSRF через http:// у сутності, «сліпий» XXE з передачею даних на зовнішній сервер, DoS «мільярдом сміхів» (експоненційно вкладені сутності).

Де трапляється XML: імпорт прайсів і фідів, SOAP-інтеграції, SAML (вхід через корпоративний SSO), формати Office (DOCX, XLSX - це zip з XML), SVG.

Сучасний PHP за замовчуванням безпечніший. З libxml 2.9 зовнішні сутності не підставляються, якщо їх не ввімкнути явно. Небезпечні - прапорці:

$doc = new DOMDocument();
$doc->loadXML($xml);                     // за замовчуванням сутності не розкриваються
$doc->loadXML($xml, LIBXML_NOENT);       // НЕБЕЗПЕЧНО: розкриває сутності, зокрема зовнішні
$doc->loadXML($xml, LIBXML_DTDLOAD);     // НЕБЕЗПЕЧНО: завантажує зовнішні DTD

Назва LIBXML_NOENT вводить в оману: «no entities» означає «замінити сутності їхнім вмістом», тобто ввімкнути підстановку. Його часто додають «щоб працювали &amp;-подібні сутності» - і відкривають XXE.

Функцію libxml_disable_entity_loader() оголошено застарілою в PHP 8.0 - вона більше не потрібна для захисту за замовчуванням.

Правила:

  • не використовувати LIBXML_NOENT і LIBXML_DTDLOAD з даними ззовні;
  • відхиляти документи з <!DOCTYPE>, якщо формату він не потрібен;
  • обмежувати розмір XML і глибину вкладеності;
  • бібліотеки для SAML і SOAP - актуальні версії: XXE в них - відомий клас вразливостей;
  • SVG від користувачів - окрема тема: окрім XXE під час обробки на сервері, SVG може містити скрипти (XSS при відкритті).

JSON замість XML, де це можливо, - менше поверхня атаки: у JSON немає сутностей і DTD.

Докладніше в документації: OWASP: запобігання XXE

У JavaScript об'єкти успадковують властивості від прототипу, і всі звичайні об'єкти мають спільний Object.prototype. Забруднення прототипу - зловмисник через дані змушує код записати властивість у сам прототип, і вона з'являється в усіх об'єктах застосунку.

Як це трапляється - рекурсивне злиття чи встановлення властивості за шляхом з даних користувача:

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      target[key] ??= {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));

({}).isAdmin;   // true - у кожного об'єкта

Ключ __proto__ вказує на прототип, тож запис іде в Object.prototype. Так само через constructor.prototype.

Наслідки:

  • обхід перевірок: if (user.isAdmin) для об'єкта, в якому поля немає, раптом дає true;
  • XSS у браузері: бібліотека читає налаштування з об'єкта опцій (options.innerHTML, options.src), а «забруднене» значення підставляється в DOM. Так атакують через параметри URL (?__proto__[x]=...) з бібліотеками, що розбирають рядок запиту у вкладені об'єкти;
  • виконання коду на сервері (Node.js): забруднені опції child_process, рушіїв шаблонів;
  • DoS - зламана логіка всього застосунку.

Захист:

  • блокувати небезпечні ключі при злитті й записі за шляхом: __proto__, constructor, prototype;
  • об'єкти без прототипу для словників з даних: Object.create(null);
  • Map замість звичайного об'єкта для довільних ключів;
  • валідація схемою (Zod) з «суворими» об'єктами - невідомі ключі відкидаються;
  • оновлювати бібліотеки злиття, розбору рядка запиту, глибокого копіювання - багато відомих CVE саме тут (lodash merge, qs, jQuery extend);
  • Object.freeze(Object.prototype) - радикальний захист, але може зламати сторонні бібліотеки.

Чим це стосується PHP-розробника: фронтенд Laravel-застосунку (Vue, React, Alpine) і Node-скрипти збирання/SSR - теж частина поверхні атаки; оновлення npm-залежностей важливе так само, як composer.

Докладніше в документації: PortSwigger: Prototype pollution