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 не вразливі: браузер не додає такий заголовок автоматично.
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 зазвичай не отримає).
Таймаути й ліміт розміру відповіді - теж обов'язкові, інакше функцію можна використати для навантаження на сервер.
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» означає «замінити сутності їхнім вмістом», тобто ввімкнути підстановку. Його часто додають «щоб працювали &-подібні сутності» - і відкривають 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.
У 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, jQueryextend); Object.freeze(Object.prototype)- радикальний захист, але може зламати сторонні бібліотеки.
Чим це стосується PHP-розробника: фронтенд Laravel-застосунку (Vue, React, Alpine) і Node-скрипти збирання/SSR - теж частина поверхні атаки; оновлення npm-залежностей важливе так само, як composer.