SSRF, завантаження файлів і XXE
Зміст
Три різні рядки — URL у полі «підтягнути картинку за посиланням», імʼя файлу у формі завантаження, XML у тілі запиту. Спільне в них одне: кожен керує дією, яку виконує сервер — сходити мережею, записати файл на диск, розібрати документ. А серверна дія успадковує все, що має сервер: місце всередині периметра, права у файловій системі, довіру сусідніх сервісів. не пробиває мережеву межу — він просить застосунок зробити це за нього.
Для QA це клас, де функціональна перевірка не бачить нічого: картинка підтягнулась, файл прийнявся, XML розібрався, статус 200. Ця глава — канонічний виклад , завантаження файлів, , , , й : інші глави посилаються сюди. Межі: категорія — глава Авторизація та контроль доступу, хибна конфігурація — глава Security misconfiguration і розкриття даних, механіка відсоткового кодування — глава URL і кодування.
SSRF: запит, який робить сам сервер
SSRF (Server-Side Request Forgery) — слабкість, за якої вебсервер отримує URL від компонента вище за потоком, тягне вміст за цим URL і недостатньо перевіряє, що запит іде саме туди, куди мав би. OWASP описує те саме з боку : це вектор, який зловживає застосунком, щоб той сам сходив у внутрішню чи зовнішню мережу або на власну машину. У старих матеріалах трапляється синонім XSPA (Cross Site Port Attack).
Чому це працює, видно з одного речення каталогу слабкостей: підставивши URL на несподіваний хост чи порт, атакувальник робить запит таким, ніби його шле сервер, — і так може обійти контроль доступу на кшталт файрвола, який не пускає його туди напряму. Далі сервер працює як : сканування портів усередині мережі, читання локальних файлів через file://, протоколи на кшталт gopher:// чи tftp://, що дають більше контролю над вмістом запиту. Цілі названо поіменно — внутрішні адмінпанелі, хмарних метаданих, локальні сервіси на localhost. Перший запит при цьому зазвичай HTTP, а другий застосунок може зробити іншою схемою (file://, phar://, gopher://, data://, dict://), тож перевірка «а це взагалі http(s)-адреса?» — окремий крок.
У хмарі SSRF типово використовують, щоб викрасти облікові дані й токени доступу із (instance metadata service) — AWS, Azure, GCP. Мінімум, який блокують у , — адреса 169.254.169.254 і доменне імʼя metadata.amazonaws.com. IMDSv2 в AWS названо додатковим шаром (defence in depth), який помʼякшує частину випадків, а не закриває клас.
SSRF у редакції 2025: чому це контроль доступу
У :2025 окремої категорії SSRF немає: зламаний контроль доступу (Broken Access Control) лишається №1, і SSRF влито саме в нього. Провідні категорії названо поіменно — розкриття чутливої інформації неавторизованому , розкриття через надіслані дані, CWE-918 SSRF і CSRF; тобто обидві «підробки запиту» тепер живуть усередині контролю доступу. Поруч стоїть цифра, яку варто читати правильно: 100% протестованих застосунків мали ту чи іншу форму зламаного контролю доступу — це частка застосунків, а не частка інцидентів.
Переїзд зафіксовано з двох боків. Каталог слабкостей показує його у власному записі: CWE-918 належав до категорії A10:2021 «Server-Side Request Forgery», а тепер належить до A01:2025 «Broken Access Control». SSRF не зник — він переїхав, тож відповідь «SSRF — це A10» без згадки року вже застаріла. Ще одна деталь каталогу корисна для пріоритизації: ordinality SSRF — Resultant, слабкість зазвичай виникає внаслідок присутності інших слабкостей, тож у розборі інциденту вона стоїть після причини, а не поруч із нею.
Захист від SSRF: два випадки й два рівні
OWASP розводить сценарії за однією ознакою — чи можливий . Якщо застосунок ходить лише до відомих і довірених застосунків, список дозволеного будується. Якщо ж він може звертатися до будь-якої зовнішньої IP чи домену — типовий приклад тут вебхуки, бо перелік адрес наперед невідомий і динамічно змінюється, — лишається deny-list, і джерело чесно називає межу: він не є непроникною стіною, це найкраще з доступного в цьому сценарії. Вимога до нього жорстка: фільтр мусить розпізнавати приватні діапазони, localhost і link-local адреси — і для IPv4, і для IPv6. Пріоритет незмінний: deny-list схильні до обходу, віддавати перевагу треба allow-list.
На рівні застосунку перший захід — через allow-list, бо очікуваний формат даних здебільшого відомий наперед. Три вимоги, які й провалюються:
- Повний URL від користувача не приймають узагалі — його важко валідувати, а парсером можна зловживати залежно від технології; якщо адреса потрібна, приймають лише валідну IP або доменне імʼя.
- Слідування редиректам вимикають — інакше дозволена адреса з редиректом на заборонену проводить запит повз уже пройдену валідацію.
- DNS-резолв — окремий вектор: ним атакувальник привʼязує легітимне доменне імʼя до внутрішньої IP-адреси.
Рівень мережі обмежує застосунок дозволеними маршрутами, щоб він фізично не міг звернутися до довільної цілі; роблять на обох рівнях разом — це і є defense in depth. Автоматика допомагає частково: (сам метод — глава Статичне тестування і ревʼю), у безпековій рамці званий (Static Application Security Testing), аналізує код без виконання й знаходить деякі входження цієї слабкості. «Деякі» — модальність джерела, а не наше застереження: чистий звіт сканера відсутності класу не доводить.
Небезпечне завантаження файлів
Вимогу сформульовано як обовʼязок застосунку, а не користувача: він має вміти відбивати підроблені й шкідливі файли, щоб і сам він, і його користувачі лишалися в безпеці. Головне застереження стоїть до будь-якого чеклиста: срібної кулі у валідації користувацького вмісту немає — захист будується як defense in depth із кількох технік одночасно.
Шкідливий файл експлуатує парсер або модуль обробки (серед прикладів — XXE), бʼє по доступності ZIP-бомбами, XML-бомбами (billion laughs) чи просто величезним розміром, перезаписує наявний файл або несе активний клієнтський вміст, небезпечний для інших користувачів, якщо файли публічні. Сама публічна доступність додає ще два ризики — розкриття чужих файлів і DoS на асиметрії, коли запити маленькі, а відповіді значно більші.
Перелік заходів джерело подає одним абзацом: allow-list розширень (лише критичні для бізнес-функції); валідація вводу перед перевіркою розширення; недовіра до Content-Type; імʼя файлу, згенероване застосунком; ліміт розміру; лише авторизовані користувачі; зберігання на іншому сервері, а якщо неможливо — поза webroot. Туди ж входять антивірус або для вмісту й захист самої форми від CSRF.
Ламається це зазвичай у трьох місцях:
- Розширення. Валідація виконується після декодування імені й має витримувати відомі обходи: подвійне розширення
.jpg.phpобходить регулярку на\.jpg, а нульовий байт у.php%00.jpgвідрізає.jpg, і новим розширенням стає.php. Блокування конкретних розширень саме собою — слабкий метод; канон вимагає allow-list типів і розширень, а виконувані серверні скрипти (asp,aspx,jsp,php,pl,cgi,js,shtmlта інші з переліку) не приймати взагалі. Content-Type. Заголовок задає користувач, підробити його тривіально — для безпеки він не годиться; лишається йому роль швидкої перевірки від ненавмисної помилки з типом.- Імʼя. Його генерує застосунок — випадковий рядок на кшталт UUID, і це названо обовʼязковим заходом. Якщо користувацьке імʼя потрібне бізнесу, воно валідується окремо під клієнтські вектори (активний вміст → XSS) і серверні (перезапис чи створення особливих файлів), а провідні й послідовні крапки обмежуються явно.
Вміст і місце: для зображень надійний захід — перезапис зображення (image rewriting), який знищує вкладений у картинку шкідливий вміст; ZIP приймати не рекомендовано взагалі; зберігати краще на окремому хості з повним розділенням обовʼязків, інакше поза webroot із доступом лише адміністратора й правами за принципом . Пастка, яку легко проґавити: ліміт розміру рахується після розпакування — саме тому ZIP-бомба проходить крізь ліміт на розмір архіву. Доступ до самої функції — дві різні перевірки, і жодна не заміняє іншу: користувач має бути зареєстрованим або принаймні ідентифікованим і мати права на ці файли. А не є «файл завантажився»: джерело описує наслідки — виконання завантаженого, перезапис чужого файлу, XSS в іншого користувача.
Path traversal: вихід за межі каталогу
Path traversal — слабкість, за якої продукт будує шлях із зовнішнього вводу, шлях має вказувати на файл під обмежувальною батьківською директорією, але спецелементи в ньому не нейтралізовано — і шлях резолвиться поза цією директорією. Механізм: спецелементами .. і роздільниками / атакувальник виходить за межі обмеженого розташування; для вебсервера ця межа — web document root, фізична директорія, у якій сервер намагається замкнути файли користувачів.
Форм дві, і в другої немає жодного ../: відносний traversal — послідовність ../, яку більшість сучасних ОС читає як батьківську директорію; абсолютний — підстановка абсолютного шляху на кшталт /usr/local/bin. Тобто «у пейлоаді немає ../» не означає «traversal немає». MITRE додає термінологічну тонкість: і «path traversal», і «directory traversal» описують атаку, а не саму слабкість; перелічує ті самі синоніми — dot-dot-slash, directory climbing, backtracking.
Наслідки зачіпають усі три властивості: читання чужих файлів (а якщо прочитаний файл обслуговує механізм безпеки — механізм обходиться); запис, яким створюють або перезаписують критичні файли, що система сама виконує; і доступність, бо перезапис чи видалення таких файлів здатні зупинити продукт. Веб додає четверте — підключення скриптів із зовнішніх сайтів (remote file include).
../ розгортає сама ОС під час канонізації шляху: застосунок «нічого не робить», а відкривається інший файл; дефект не в конкатенації, а у відсутності перевірки параметра, який туди потрапляє. Для успіху потрібні дві умови разом — провалена валідація і достатні права застосунку на цільовий файл.
Кодування — окремий вимір, і саме на ньому ламається більшість фільтрів. Перелік схем, які WSTG вимагає перебрати, починається з URL-кодування й подвійного URL-кодування: %2e%2e%2f і ..%2f — це той самий ../; нульовий байт %00 названо окремо як спосіб обійти контроль розширення; Unicode/UTF-8-кодування працює лише там, де система приймає надлишково довгі UTF-8-послідовності. Правило звідти пряме: типова помилка розробника — врахувати лише базове кодування, тож не спрацював рядок — міняй схему кодування, а не висновок.
Захист. Базова стратегія — «accept known good»: список прийнятних значень, що строго відповідають специфікації; для імен файлів це allow-list символів, щонайбільше одна крапка в імені, виключені роздільники шляху, allow-list розширень. Порядок обовʼязковий: декодування й канонізація до внутрішнього представлення, і тільки потім валідація (подвійне декодування — окремий дефект). Правильний інструмент — вбудована канонізація шляху (realpath() і аналоги), яка знімає і .., і символьні посилання. «Вирізати небезпечні символи» — це deny-list, і він неповний: фільтр на / не рятує там, де роздільником працює й \, а рядок .../...// після одноразового вирізання складається назад у ../. Клієнтська перевірка не рахується. Найсильніший захід — не пускати шлях від користувача взагалі: мапа «фіксований ID → реальний файл», а шлях зберігання задає сервер; службові файли й бібліотеки лежать поза web document root.
Дві межі методики. Тест залежить від платформи: просити /etc/passwd в IIS немає сенсу, тож негативний результат без знання системи нічого не доводить. Частина дефектів цього класу без доступу до коду не знаходиться взагалі — WSTG радить у шукати вектори грепом по інклюд-функціях і файлових операціях, окремо переглядаючи функції, що «лагодять» невалідний ввід замість відкидати; інструменти гайд називає поіменно — DotDotPwn, набори fuzz-рядків із WFuzz, OWASP , . Окремий випадок — traversal другого порядку: рядок кладеться в базу, а спрацьовує пізніше, коли параметр усередині інклюд-функції «виглядає внутрішнім і безпечним».
XXE: зовнішні сутності XML
XXE (XML eXternal Entities) — слабкість, за якої продукт обробляє XML-документ із сутностями, чиї URI резолвляться в документи поза задуманою сферою контролю, і вбудовує чужі документи у власний вивід. OWASP формулює те саме парою умов: XXE стається, коли недовірений XML із посиланням на зовнішню сутність обробляє слабко налаштований XML-парсер.
Механізм закладений у самому форматі — тому це не «баг парсера», а його штатна можливість, залишена ввімкненою. Каталог слабкостей описує ланцюг так: XML-документ може містити DTD, DTD дозволяє оголосити сутність через підстановку у формі URI, а парсер має право сходити за цим URI й підставити вміст назад у документ.
Наслідків три. Читання довільних файлів: сутність зі схемою file:// змушує застосунок прочитати локальний файл, а витік стається там, де застосунок віддає прочитане назад — зокрема в тексті повідомлення про помилку. Вихідні запити руками застосунку: схема http:// дає обхід обмежень файрвола, приховане сканування портів і зловживання довірою сервера до сусідів. в обслуговуванні: URI на величезний файл або на пристрій, що завжди віддає дані, плюс вкладені рекурсивні посилання на сутності. Матриця наслідків OWASP додає критичне для тестування: дозволене завантаження зовнішніх DTD дає (Blind XXE) — результату у видимій відповіді немає взагалі.
Межа з SSRF: обидві слабкості породжують вихідні запити в неочікувані призначення, і вразливий до XXE застосунок експлуатують саме для того, щоб виконати SSRF-атаку. Але XXE буває й на клієнті або в контекстах, де ПЗ не виступає сервером, тож частина «Server» з абревіатури SSRF до такого випадку не завжди застосовна; класифікаційно XXE — Primary проти Resultant у SSRF.
Захист конфігураційний, і питання до системи ставиться не «чи є XXE», а «які налаштування парсера». Найнадійніше — повністю вимкнути DTD, що заразом закриває DoS типу billion laughs; якщо не можна, окремо вимикають зовнішні сутності й зовнішні оголошення типу документа. Мінімальний чекліст харденінгу читається як готовий перелік перевірок: вимкнути DOCTYPE, зовнішні сутності, завантаження зовнішніх DTD і XInclude; увімкнути secure processing; обмежити розкриття сутностей; не брати застарілі парсери; ніколи не парсити недовірений XML із дефолтними налаштуваннями.
Дефолти залежать від платформи, і це предмет перевірки: у Java більшість XML-парсерів мають XXE увімкненим за замовчуванням, а FEATURE_SECURE_PROCESSING поводиться по-різному в різних реалізаціях — від вичерпання ресурсів може допомогти, а від розкриття сутностей рятує не завжди; PHP 8.0 і новіші з дефолтним парсером (libxml2) блокують XXE самі. Версія рантайму змінює вердикт, тож результат перевірки привʼязується до неї; сторонні бібліотеки теж парсять XML і перевіряються окремо.
Місце в мапі ризиків змінилося так само, як у SSRF: у редакції 2017 XXE мав власну категорію A4, а в редакції 2025 віднесений до A02 Security Misconfiguration, де CWE-611 названо серед провідних CWE категорії. Логіка переїзду видна з механізму — ламає не код, а налаштування; помʼякшення теж конфігураційне: багато парсерів можна налаштувати так, щоб розкриття зовнішніх сутностей було вимкнене.
Валідація на сервері: allow-list проти deny-list
Валідація вводу ( validation) — перевірка, що в обробку потрапляють лише коректно сформовані дані, виконувана якомога раніше, бажано одразу на прийомі від зовнішньої сторони. Межу OWASP називає сам: валідація вводу не є основним методом запобігання XSS чи SQL-інʼєкції. Для тем цієї глави ситуація дзеркальна — тут вона названа саме першим заходом.
Рівнів два: синтаксичний (правильний синтаксис структурованих полів — номер, дата, символ валюти) і семантичний (коректність значень у бізнес-контексті: дата початку раніша за дату кінця, ціна в очікуваному діапазоні). Потрібні обидва, бо формально валідна дата може бути беззмістовною в сценарії. І недовіреним джерелом є не лише браузер: валідують дані з бекенд-фідів від постачальників, партнерів і вендорів.
Allow-list описує рівно те, що дозволено, і все інше за визначенням заборонене; підхід доречний для всіх полів вводу від користувача, а найпростіша його форма — масив дозволених значень. Deny-list — перелік відомих небезпечних шаблонів, і позиція джерела однозначна: ловити «небезпечні» символи (', 1=1) — украй хибний підхід, бо такі фільтри тривіально обходяться, а на легітимних даних дають хибні спрацювання (класика — прізвище O'Brian, де апостроф законний). Роль, яку джерело за deny-list лишає, — додатковий шар, що доповнює, а не заміщає allow-list; винятком лишається вузький випадок, коли allow-list неможливий за природою функції.
Валідацію реалізують на сервері, до того як дані потраплять у функції застосунку: будь-яку перевірку на JavaScript обходять вимкненим JS або вебпроксі. Рекомендований підхід — обидві: клієнтська для UX, серверна для безпеки. І деталь, яку варто нести в баг-репорт: якщо поле походить із фіксованого набору (випадний список, радіокнопки), невідповідність на сервері — подія високої критичності, яку логують як таку, бо вона свідчить про втручання в клієнтський код.
// Playwright: та сама вимога повз UI має відхилятися сервером
const res = await request.post('/api/orders', {
data: { deliveryOption: 'FREE_OVERNIGHT' }, // варіанта немає у списку опцій
});
expect(res.ok()).toBeFalsy();
Техніки складаються в готовий набір негативних перевірок: діапазон для чисел і дат, мінімальна й максимальна довжина рядків, дозволений набір символів. Для вільного тексту першим кроком іде нормалізація, а allow-list для Unicode будується за категоріями символів («літери», «десяткові цифри»), що покриває не лише латиницю. Порядок кроків обовʼязковий: ввід декодують і канонізують перед валідацією й не декодують двічі.
Сусідні класи: відкритий редирект і небезпечна десеріалізація
Відкритий редирект (open redirect). Неперевірені редиректи й форварди можливі там, де застосунок приймає недовірений ввід, здатний змусити його перенаправити запит на URL із цього вводу; змінивши адресу на шкідливий сайт, атакувальник розкручує фішинг і краде облікові дані. Ефективність — саме в довірі: імʼя сервера в підробленому посиланні збігається з оригінальним сайтом, тож користувач бачить знайомий домен і не здогадується про перенаправлення. Друга грань — обхід контролю доступу: підготовлений URL проходить перевірку й форвардить атакувальника до привілейованих функцій, тому для серверного форварду джерело вимагає перевіряти три речі — право користувача на цей URL, право на функції, які URL надає, і доречність самого запиту.
Безпечним вважається редирект, чия адреса явно оголошена в коді й не піддається маніпуляції. Заходи подані за спаданням сили: не використовувати редиректи й форварди взагалі; не дозволяти URL як користувацький ввід для цілі; передавати коротке імʼя, ID або токен, який на сервері мапиться на повну адресу — хоч у цього кроку є власний ризик, названий відразу: перебором ID користувач дізнається всі можливі цілі. Якщо вводу не уникнути, значення перевіряють на валідність, доречність і право саме цього користувача, а санітизація робиться списком довірених адрес, побудованим як allow-list, а не як deny-list; запасний захід — проміжна сторінка з показаною ціллю, де перехід підтверджують кліком. Різницю з SSRF тримайте чіткою: тут перенаправляється запит користувача, і саме він не помічає підміни, а в SSRF запит робить сам застосунок; спільна в них лише проблема валідації URL, чию нетривіальність шпаргалка про редиректи визнає прямо, відсилаючи по деталі до шпаргалки про SSRF.
Небезпечна десеріалізація (insecure deserialization). Серіалізація — перетворення обʼєкта у формат даних, який можна відновити пізніше; десеріалізація — зворотний хід. Дефект живе не в самій операції, а в рідних форматах мов: вони дають більше можливостей за JSON чи XML, зокрема кастомізацію самого процесу серіалізації, — і саме це обертають проти застосунку. Наслідком названо відмову в обслуговуванні, обхід контролю доступу або віддалене виконання коду.
Канонічне питання до розробника одне: чи визначає тип обʼєкта сам потік даних. Перевірка типу після десеріалізації запізніла — у деяких типах код виконується просто під час неї, тож перевірку ставлять до відбудови обʼєкта. Найбільше зниження ризику дає відмова від рідних форматів на користь чистих даних; якщо перелік повідомлень відомий наперед, їх підписують і не десеріалізують без автентифікованого підпису. Аргумент «ми додали фільтр небезпечних класів» джерело знецінює наперед: нові класи-ґаджети знаходитимуть і далі. А дешева перевірка без доступу до коду — сигнатури в перехопленому трафіку: AC ED 00 05 у hex, префікс rO0 у , заголовок Content-Type: application/x-java-serialized-object; дані pickle у Base64 найімовірніше починаються з gASV.
Типові помилки
- «Ми валідуємо URL — SSRF закритий» → виглядає як захист, а насправді канон вимагає не приймати повний URL узагалі; до того ж редирект із дозволеної адреси й DNS-імʼя, привʼязане до внутрішньої IP, обходять уже пройдену перевірку.
- «У пейлоаді немає
../— traversal неможливий» → виглядає як негативний результат, а насправді абсолютний шлях дає той самий ефект без крапок, а%2e%2e%2fі..%2f— це той самий../в іншому кодуванні. - «Файл завантажився зі статусом
200» → виглядає як пройдений тест, а насправді оракулом є наслідок: виконання завантаженого, перезапис чужого файлу, XSS в іншого користувача. - «Ліміт розміру стоїть» → виглядає як захист від бомб, а насправді рахувати треба розмір після розпакування — інакше ZIP-бомба проходить крізь ліміт на архів.
- «Відповідь порожня — XXE немає» → виглядає як негативний результат, а насправді дозволене завантаження зовнішніх DTD дає сліпий XXE, у якого видимого результату не буває за побудовою.
- «Фільтр небезпечних символів є» → виглядає як валідація, а насправді це deny-list: він обходиться й ламає легітимні дані на кшталт
O'Brian, тоді як канон — allow-list. - «Сканер пройшов чисто» → виглядає як доказ, а насправді SAST знаходить лише деякі входження слабкості, а частина дефектів цього класу без доступу до коду не знаходиться взагалі.
Підсумок
- Спільний корінь усіх тем глави — недовірений рядок, який керує серверною дією. URL, імʼя файлу і XML різні за синтаксисом, але однакові за наслідком: дію виконує сервер, і вона успадковує його права та місце в мережі.
- У редакції 2025 ні SSRF, ні XXE не мають власної категорії: SSRF влито в A01 Broken Access Control, XXE — в A02 Security Misconfiguration. Обидва переїзди зафіксовані і в списку, і в записах каталогу, тож категорію називайте з роком редакції.
- Захист будується на сервері й за списком дозволеного. Повний URL від користувача не приймають, шлях від користувача не пускають у файлову операцію, розширення й типи задає allow-list; deny-list лишається додатковим шаром і стає основним лише там, де перелік дозволеного невідомий наперед.
- Порядок кроків важить не менше за самі перевірки. Декодування й канонізація йдуть перед валідацією, ліміт розміру рахується після розпакування, перевірка типу при десеріалізації ставиться до відбудови обʼєкта.
- Негативний результат тут слабкий за побудовою. Сліпий XXE не має видимої відповіді, traversal залежить від платформи, SAST знаходить лише частину входжень — тож «не відтворилося» без контексту системи не доводить нічого.
Можливі питання
- «Що таке SSRF і чим він небезпечний, якщо застосунок і так ходить у мережу?» — перевіряють, чи бачите ви суть: небезпечна не сама вихідна дія, а те, від чийого імені вона виконується. Сильна відповідь говорить про запит із довіреного сервера, що обходить контроль на кшталт файрвола, і називає типові цілі — внутрішні адмінки,
localhost, сервіс метаданих. - «До якої категорії OWASP Top 10 належить SSRF?» — питання-датчик актуальності знань. Чекають згадку редакції: власна категорія A10 у 2021, а в 2025 — усередині A01 Broken Access Control.
- «Як ви перевірятимете форму завантаження файлів?» — чекають не «завантажу .exe», а перелік вимірів: розширення й два класичні обходи, недовіра до
Content-Type, імʼя й місце зберігання, ліміт після розпакування, права на саму функцію — і оракул за наслідком, а не за статусом відповіді. - «Фільтр вирізає
../. Цього досить?» — пастка на однослівне «так». Сильна відповідь називає абсолютні шляхи, ,%00, рядок.../...//, який складається назад, і правильний порядок: декодувати й канонізувати, потім валідувати. - «Що таке XXE і чому його відносять до хибної конфігурації?» — на розуміння, що механізм штатний: DTD із зовнішньою сутністю, а парсер із увімкненим дефолтним резолвером сутностей її підтягує, тож питання не «чи є дефект у коді», а «як налаштований парсер» — і відповідь залежить від платформи й версії рантайму.
- «Чим відкритий редирект відрізняється від SSRF?» — на точність меж: у першому перенаправляється запит користувача, у другому запит робить сам застосунок; спільна в них лише проблема валідації URL.
Джерела
SSRF: запит, який робить сам сервер
- CWE-918 — Server-Side Request Forgery (SSRF) — означення слабкості, синонім XSPA, обхід файрвола, сервер як проксі, перелік типових цілей.
- OWASP — Server-Side Request Forgery Prevention Cheat Sheet — SSRF як зловживання застосунком, другий запит іншою схемою, крадіжка з сервісів метаданих,
169.254.169.254і межа IMDSv2.
SSRF у редакції 2025: чому це контроль доступу
- OWASP Top 10:2025 — Introduction — Broken Access Control лишається №1, SSRF влито в цю категорію.
- OWASP Top 10:2025 — A01 Broken Access Control — провідні CWE категорії, зокрема CWE-918 і CSRF, і частка протестованих застосунків.
- CWE-918 — Server-Side Request Forgery (SSRF) — членство в категоріях A10:2021 і A01:2025, обхід контролю доступу, ordinality
Resultant.
Захист від SSRF: два випадки й два рівні
- OWASP — Server-Side Request Forgery Prevention Cheat Sheet — два випадки й межа deny-list, вимоги до фільтра адрес, заборона приймати повний URL, вимкнені редиректи, DNS як вектор, рівень мережі та defense in depth.
- CWE-918 — Server-Side Request Forgery (SSRF) — назва методу SAST і його модальність: знаходить деякі входження слабкості.
Небезпечне завантаження файлів
- OWASP Cheat Sheet — File Upload — обовʼязок застосунку й відсутність срібної кулі, шкідливого файлу, перелік заходів, обходи розширення, недовіра до
Content-Type, генероване імʼя, перезапис зображення, місце зберігання, ліміт після розпакування, дві перевірки доступу. - OWASP Cheat Sheet — Input Validation — allow-list типів і розширень, перелік виконуваних серверних скриптів, заборона керованого користувачем тексту в імені на диску.
Path traversal: вихід за межі каталогу
- CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') — означення слабкості, відносна й абсолютна форми, термінологічна межа, наслідки за трьома властивостями, канонізація силами ОС, «accept known good», порядок «декодувати → валідувати», межі deny-list і мапа замість шляху.
- OWASP WSTG — 4.5.1 Testing Directory Traversal File Include (WSTG-ATHZ-01) — web document root як межа, синоніми, remote file include, дві умови успіху, схеми кодування й правило «міняй кодування», залежність від платформи, gray-box і межа , названий інструментарій, traversal другого порядку.
- OWASP Cheat Sheet — Input Validation — шлях зберігання задає сервер, а не клієнт.
- CWE-611 — Improper Restriction of XML External Entity Reference — означення слабкості, механізм із DTD і URI, три наслідки, витік через текст помилки, межа з SSRF і ordinality
Primary, категорії A4:2017 і A02:2025, конфігураційне помʼякшення. - OWASP — XML External Entity Prevention Cheat Sheet — пара умов виникнення, матриця наслідків і сліпий XXE, повне вимкнення DTD, мінімальний чекліст харденінгу, дефолти Java й PHP, сторонні бібліотеки.
- OWASP — Server-Side Request Forgery Prevention Cheat Sheet — вразливий XML-парсер як шлях до виконання SSRF-атаки.
- OWASP Top 10:2025 — A02 Security Misconfiguration — CWE-611 серед провідних CWE категорії хибної конфігурації.
Валідація на сервері: allow-list проти deny-list
- OWASP Cheat Sheet — Input Validation — мета й раннє місце валідації, її межа щодо XSS і , синтаксичний і семантичний рівні, недовірені бекенд-фіди, означення allow-list і deny-list з обома вадами, клієнт проти сервера, логування невідповідності як події високої критичності, техніки й категорії Unicode.
- CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') — порядок «декодувати й канонізувати перед валідацією», дублювання клієнтських перевірок на сервері, мапа «ID → обʼєкт».
- OWASP — Server-Side Request Forgery Prevention Cheat Sheet — перевага allow-list і вузька умова, за якої беруть deny-list.
Сусідні класи: відкритий редирект і небезпечна десеріалізація
- OWASP — Unvalidated Redirects and Forwards Cheat Sheet — означення й механіка фішингу, форвард повз контроль доступу і три перевірки, ознака безпечного редиректу, ієрархія заходів із ризиком перелічування, список довірених адрес як allow-list, проміжна сторінка, відсилання до шпаргалки про SSRF.
- OWASP — Server-Side Request Forgery Prevention Cheat Sheet — запит робить сам застосунок, а повний URL від користувача не приймають.
- OWASP — Deserialization Cheat Sheet — означення серіалізації й десеріалізації, ризик рідних форматів, наслідки, правило про тип із потоку, перевірка до відбудови обʼєкта, підпис даних, приреченість deny-list, сигнатури в трафіку.
Що таке SSRF і чому запит, зроблений сервером, небезпечніший за такий самий запит від атакувальника?
(Server-Side Request Forgery) — ситуація, коли застосунок бере адресу з вводу, сам іде за нею мережею й недостатньо перевіряє, куди саме потрапив. Небезпечна тут не сама вихідна дія: сервіси постійно тягнуть чужі картинки, шлють вебхуки, ходять у сторонні API. Небезпечно те, від чийого імені відбувається похід. Сервер стоїть усередині периметра, і мережеві правила, які не пускають зовнішнього гостя до внутрішньої адмінки, на нього не поширюються. Тобто не ламає файрвол — він просить довірену машину сходити замість себе, і каталог слабкостей класифікує це саме як обхід контролю доступу. Далі застосунок працює як : ним сканують порти всередині мережі, стукають на localhost, витягують локальні файли схемою file://. Для QA висновок простий: будь-яке поле, куди лягає адреса — «підтягнути зображення за посиланням», URL вебхука, callback після оплати, — кандидат на перевірку, і функціонально воно виглядатиме бездоганно.
Що саме атакувальник шукає через SSRF у хмарній інфраструктурі?
Найласіша ціль — (instance metadata service), який є в AWS, Azure і GCP і відповідає на link-local адресу, доступну лише з самого інстанса. Сходивши туди руками застосунку, атакувальник забирає облікові дані й токени тієї ролі, під якою крутиться інстанс, — і це вже не «прочитав чужу картинку», а робочий доступ до хмарного акаунта з правами сервісу. Мінімальний набір, який тримають у переліку заборонених адрес, — 169.254.169.254 і metadata.amazonaws.com, хоча перелічити всі способи записати ту саму адресу складніше, ніж здається. IMDSv2 в AWS ситуацію покращує, але OWASP описує його як додатковий шар захисту (defence in depth): він знімає частину сценаріїв, а не закриває клас. Практичний наслідок: адреса метаданих має лежати в негативному наборі кожної форми, що приймає URL, — це найдешевша проба з найбільшою ціною знахідки.
У якій категорії OWASP Top 10 сьогодні живе SSRF?
Відповідь без згадки року вже застаріла — саме це питання й перевіряє. У редакції 2021 SSRF мав власну категорію A10, а в редакції 2025 окремої категорії в нього немає: його влили в A01 Broken Access Control, який лишився номер один. Логіка переїзду читається з механізму — SSRF дає дістатися ресурсу, доступ до якого мав бути закритий, тобто це , а не окремий звір. Запис -918 показує обидва членства одночасно, тож перевіряється це за хвилину. Поруч зазвичай цитують цифру «100% протестованих застосунків»: читати її треба як частку застосунків, у яких знайшли хоч якусь форму зламаного контролю доступу, а не як частку інцидентів. І ще деталь для пріоритизації: SSRF має ordinality Resultant — слабкість зазвичай виростає з іншої, тому в розборі інциденту вона стоїть після першопричини, а не замість неї.
«Ми валідуємо URL, отже SSRF закритий». Що ви заперечите?
Що валідація повного URL — найслабший варіант із можливих, і канон радить повний URL від користувача не приймати взагалі. По-перше, надійно розібрати URL важко: парсери різних мов розходяться на крайніх випадках, і цим зловживають. По-друге, перевірка одноразова, а похід — ні: дозволена адреса може відповісти редиректом на внутрішню, і застосунок піде за ним уже повз пройдену валідацію, тому слідування редиректам вимикають окремо. По-третє, між перевіркою і запитом стоїть DNS — цілком легітимне доменне імʼя атакувальник прив'язує до внутрішньої IP-адреси. Окремо перевіряють схему: перший запит іде по HTTP, а наступний застосунок здатен зробити зовсім іншим протоколом (file://, gopher://, dict://). Якщо адреса справді потрібна бізнесу, приймають не рядок URL, а валідну IP чи доменне імʼя, звірене зі списком дозволеного.
Коли для SSRF доводиться брати deny-list і які до нього вимоги?
Тоді, коли перелік цілей неможливо скласти наперед. Канонічний приклад — вебхуки: адресу вказує сам користувач, вона змінюється динамічно, і просто нема з чого будувати. OWASP такий сценарій визнає й дозволяє , але одразу називає межу: непроникною стіною він не буде, це найкраще з доступного саме тут. Вимоги до фільтра жорсткі — він мусить упізнавати приватні діапазони, localhost і link-local адреси, причому однаково в IPv4 і IPv6: вимога охоплює обидві версії разом, тож фільтр, написаний лише під IPv4, її не виконує. Там, де застосунок спілкується лише з кількома відомими сервісами, вибір інший і однозначний: allow-list, бо deny-list за побудовою схильні до обходу. І в обох випадках лишається другий рівень — мережевий: застосунку залишають тільки дозволені маршрути, щоб він фізично не дотягнувся до довільної цілі.
Як ви будете тестувати форму завантаження файлів?
Не «спробую залити .exe», а піду по вимірах, бо дефект може сидіти в будь-якому з них. Перший вимір — тип і розширення: перевіряю, чи приймається виконуваний серверний скрипт (php, jsp, aspx тощо) і чи витримує фільтр два класичні обходи — подвійне розширення й нульовий байт. Другий — довіра до Content-Type: підміняю заголовок і дивлюсь, чи цього достатньо, щоб змінити вердикт. Третій — імʼя й місце: чи потрапляє моє імʼя на диск дослівно, чи можна пройти по ньому в сусідній каталог, чи лежить результат усередині webroot і чи віддається за передбачуваною адресою. Четвертий — розмір і бомби, включно з архівом, який маленький до розпакування. П'ятий — доступ: чи може завантажити хтось неавторизований і чи може один користувач писати у файли іншого. І головне — : «завантажилось зі статусом 200» нічого не доводить, дивитися треба на наслідок — чи виконався завантажений файл, чи перезаписався чужий, чи прилетів XSS іншому користувачеві.
Чому Content-Type не годиться як перевірка типу завантаженого файлу?
Бо цей заголовок формує клієнт, а не сервер, і підмінити його — справа одного рядка в проксі чи в HTTP-клієнті. Тобто це не властивість файлу, а твердження користувача про файл — а користувач у моделі недовірений. Класичний сценарій: скрипт із розширенням, яке пройшло слабку перевірку, віддають із Content-Type: image/png, і фільтр, що дивиться лише на заголовок, пропускає його. Роль, яку заголовку залишають, скромна: швидкий відсів ненавмисної помилки, коли користувач переплутав файл. Реальний вердикт мають виносити allow-list розширень і типів плюс перевірка самого вмісту — для зображень надійним заходом вважають перезапис зображення (image rewriting), який знищує все вкладене всередину картинки.
Валідація розширення зроблена регуляркою на .jpg. Які класичні обходи ви назвете?
Два, і обидва старші за більшість застосунків. Перший — подвійне розширення: імʼя shell.jpg.php містить .jpg, регулярка задоволена, а сервер дивиться на останнє розширення й бачить php. Другий — нульовий байт: у shell.php%00.jpg після декодування рядок обривається на нулі, «хвіст» .jpg відпадає, і на диск лягає .php. Звідси дві вимоги, які часто плутають між собою. Перша — порядок: імʼя спершу декодують і нормалізують, і лише потім перевіряють, інакше перевіряється не той рядок, що потрапить у файлову операцію. Друга — напрям списку: блокувати «погані» розширення марно, працює allow-list дозволених, а виконувані серверні скрипти не приймають узагалі, незалежно від того, як вони назвалися.
Чому імʼя збереженого файлу має генерувати застосунок, а не користувач?
Бо імʼя користувача — це ще один недовірений рядок, який керує серверною дією, тільки замість мережевого походу тут запис на диск. Через нього перезаписують наявний файл, створюють файл із небезпечним розширенням, вилазять у сусідній каталог послідовністю крапок, а якщо імʼя потім показується іншим користувачам — тягнуть у сторінку активний вміст, тобто XSS. Тому канон вимагає генерувати імʼя самому застосунку — випадковий рядок на кшталт UUID, — і називає це обов'язковим заходом, а не рекомендацією. Якщо бізнес усе-таки вимагає користувацьке імʼя на диску, його валідують окремо під два класи векторів — клієнтські й серверні — і явно обмежують провідні та послідовні крапки.
На завантаженні стоїть ліміт розміру. Чи захищає він від ZIP-бомби?
Ні, якщо рахується розмір того, що приїхало. У цьому й весь фокус бомби: архів на кілька сотень кілобайт розпаковується в гігабайти, тож ліміт на завантажений файл він проходить чесно, а місце й памʼять з'їдає вже після розпакування. Правило звідси пряме — ліміт застосовують до розпакованого обсягу, а не до розміру архіву. Той самий клас існує і в XML: рекурсивні сутності (billion laughs) роздувають маленький документ у величезне дерево під час парсингу. До речі, повне вимкнення DTD закриває цей сценарій заразом із . А найпростіша порада щодо архівів у джерелі звучить іще жорсткіше: приймати ZIP не рекомендують узагалі, якщо без цього можна обійтися.
Що таке path traversal і чому «у пейлоаді немає ../» нічого не доводить?
— слабкість, за якої застосунок будує шлях до файлу з зовнішнього вводу, не нейтралізувавши спецелементи, і шлях резолвиться поза дозволеною директорією; для вебсервера цією межею є web document root. Розгортає послідовність крапок не застосунок, а сама операційна система під час канонізації шляху — тому код «нічого не робить», а відкривається чужий файл: винна не склейка рядків, а параметр, який ніхто не перевірив перед нею. Форм у слабкості дві, і саме тому «немає ../» — не вердикт: крім відносної, є абсолютна, коли замість шляху всередині каталогу підставляють повний шлях на кшталт /etc/shadow, і жодних крапок там немає. Друга причина — кодування: закодований запис %2e%2e%2f або напівкодований ..%2f розкриваються в той самий перехід угору, а фільтр, який стоїть до декодування, бачить лише нешкідливий текст. Термінологічна дрібниця, яку цінують на співбесіді: «path traversal» і «directory traversal» у MITRE описують атаку, а сама слабкість — це CWE-22.
Фільтр вирізає ../ із параметра. Цього достатньо?
Ні, і це пастка на однослівне «так». Вирізання — класичний deny-list, а він неповний за побудовою. Найвідоміший контрприклад: рядок .../...// після одноразового видалення підпослідовностей складається назад у ../ — фільтр сам зібрав пейлоад. Далі йде роздільник: фільтр на / не рятує там, де система приймає й \. Далі кодування: одинарне й , %00 для обходу контролю розширення, надлишково довгі UTF-8-послідовності там, де система їх приймає. Правильна відповідь описує не кращий фільтр, а іншу конструкцію: декодувати й канонізувати шлях штатним засобом (realpath() і аналоги, які знімають і крапки, і симлінки), потім звірити, що результат лежить усередині дозволеної директорії, і лише тоді відкривати. Ще надійніше взагалі прибрати користувацький шлях із файлової операції: клієнт передає ідентифікатор, а відповідність «ідентифікатор → файл» тримає сервер.
Чому декодування й канонізація йдуть перед валідацією, а не після?
Бо валідувати треба рівно той рядок, який дійде до небезпечної функції, а не його маскування. Якщо перевірка стоїть першою, вона бачить %2e%2e%2f — з погляду регулярки це звичайний текст без крапок і слешів, — а вже після неї платформа декодує рядок у ../, і в файлову операцію їде те, що перевірку ніколи не проходило. Той самий порядок діє в завантаженні файлів: імʼя спершу нормалізують, потім звіряють розширення. Є й дзеркальна помилка — декодувати двічі: тоді пейлоад із подвійним кодуванням проходить перевірку в проміжному вигляді й розкривається на другому проході, що вважають окремим дефектом. Практичне формулювання для баг-репорта: не «фільтр слабкий», а «валідація виконується до канонізації, тому перевіряється не той рядок, що потрапляє у файлову операцію».
Що таке XXE і чому це не називають багом парсера?
XXE (XML eXternal Entities) — обробка XML-документа, у якому оголошено зовнішню сутність, чий URI веде за межі задуманої сфери контролю, і парсер сумлінно йде за цим URI й підставляє чужий вміст у документ. Багом парсера це не називають, бо механізм закладено у форматі: XML DTD, DTD дозволяє оголосити сутність через підстановку у формі URI, а підтягнути цей URI й вставити вміст назад у документ — штатне право парсера. Дефект — не в коді бібліотеки, а в тому, що ця можливість лишилася ввімкненою для недовіреного вводу. Звідси й формулювання умов: XXE стається там, де недовірений XML зустрічає слабко налаштований парсер. Наслідків три — читання локальних файлів через file:// (витік стається там, де застосунок повертає прочитане, зокрема в тексті помилки), вихідні запити руками застосунку через http:// і в обслуговуванні на величезному чи нескінченному джерелі даних. І тому питання до системи ставиться не «чи є в нас XXE», а «як налаштований парсер».
На XML-пейлоад із зовнішньою сутністю відповідь порожня. Чи означає це, що XXE немає?
Ні — це рівно той випадок, коли негативний результат слабкий за побудовою. Якщо парсер приймає зовнішні DTD, існує (Blind XXE): сутність резолвиться, запит іде, а у видимій відповіді результату немає взагалі, бо застосунок прочитане назад не друкує. Оракул тут зміщується з тіла відповіді на out-of-band-канал (OOB): піднімають підконтрольний хост і дивляться, чи прийшов на нього запит від сервера застосунку, чи спрацював DNS-резолв імені, яке більше нізвідки взятися не могло. Другий бік того ж самого — витік у текст помилки: іноді вміст файлу приїжджає не в даних, а в повідомленні про збій парсингу. І третій шлях, найдешевший: перевести питання з чорної скриньки в конфігураційне — подивитися, які прапорці стоять у парсера й яка версія рантайму, бо саме там лежить відповідь.
Від чого залежить вердикт «наш парсер захищений від XXE»?
Від платформи, версії й конкретної бібліотеки — тому вердикт без цих трьох речей не має сенсу. У Java більшість XML-парсерів історично мають зовнішні сутності увімкненими за замовчуванням, і на це треба дивитися окремо; FEATURE_SECURE_PROCESSING теж не універсальний — від вичерпання ресурсів він допомагає, а від розкриття сутностей рятує не в кожній реалізації. У PHP 8.0 і новіших дефолтний парсер на libxml2 блокує XXE самотужки, тож той самий код на різних версіях дає різний результат — і саме тому результат перевірки прив'язують до версії рантайму, а не до назви мови. Окремо перевіряють сторонні бібліотеки: XML парсять не лише «парсери», а й конвертери документів, SOAP-клієнти, обробники офісних форматів. Мінімальний перелік того, що має бути вимкнено, читається як готовий чеклист: DOCTYPE, зовнішні сутності, завантаження зовнішніх DTD, XInclude; плюс увімкнена безпечна обробка, обмежене розкриття сутностей і жодного парсингу недовіреного XML із налаштуваннями за замовчуванням.
Чим XXE відрізняється від SSRF, якщо обидва породжують вихідні запити?
Тим, що це слабкості різного рівня, які часто зустрічаються в одному ланцюжку. XXE — причина: погано налаштований парсер сам по собі є дефектом, у каталозі він позначений як Primary. SSRF — наслідок: у каталозі це Resultant, слабкість, що зазвичай виникає через іншу, і вразливий до XXE застосунок якраз і використовують, щоб виконати SSRF-атаку через схему http:// у сутності. Друга відмінність — контекст: SSRF за назвою серверна історія, а XML розбирають і на клієнті, і в компонентах, які сервером не є, тож частина «Server» до XXE підходить не завжди. Третя — місце в мапі ризиків 2025: SSRF значиться в категорії A01 (Broken Access Control), а XXE — в A02 (Security Misconfiguration), бо ламає його налаштування, а не код. Практично це впливає на тікет: у ньому першопричиною варто називати конфігурацію парсера, а внутрішній похід описувати як наслідок.
У чому різниця allow-list і deny-list і чому фільтр «небезпечних символів» вважають поганою ідеєю?
Allow-list перелічує дозволене, і будь-яке значення поза переліком відкидається автоматично; deny-list навпаки збирає відомі погані шаблони, а все, чого в ньому немає, проходить. Різниця в тому, на чиєму боці вичерпність: скласти повний перелік дозволених значень зазвичай реально (найпростіша форма — масив опцій), а повний перелік способів записати атаку — ні, і кожне нове кодування чи роздільник додає обхід. Фільтр «небезпечних символів» ламається з двох боків одразу: обходиться іншим записом того самого пейлоада — і при цьому псує легітимні дані, класика тут прізвище O'Brian, у якому апостроф абсолютно законний. Роль, яку deny-list зберігає, — додатковий шар поверх allow-list; основним він стає лише там, де перелік дозволеного невідомий за природою функції. Дві межі, які варто називати разом із цим: валідацію виконують на сервері, бо клієнтська обходиться вимкненим JS або проксі, а якщо поле походить із фіксованого набору на кшталт випадного списку, невідповідність на сервері логують як подію високої критичності — вона означає втручання в клієнтський код.
Чим відкритий редирект відрізняється від SSRF?
Тим, хто робить запит. У перенаправляється запит користувача: він клікає посилання, у якому імʼя сервера збігається зі знайомим сайтом, довіряє йому — і опиняється на сторінці атакувальника, де віддає пароль. У SSRF запит робить сам застосунок, і користувача в сценарії може не бути взагалі. Спільне в них одне — задача валідації URL, і шпаргалка про редиректи це визнає, відсилаючи за деталями до шпаргалки про SSRF. Другий бік відкритого редиректу — не фішинг, а обхід контролю доступу: підготовлена адреса проходить перевірку й форвардить атакувальника до привілейованої функції. Тому серверний форвард обкладають трьома окремими перевірками: чи має цей користувач право на саму адресу, чи має він право на функціональність, до якої вона веде, і чи доречний такий запит узагалі. Безпечним вважають редирект, чия ціль явно прописана в коді; якщо без вводу не обійтися, клієнт передає короткий ідентифікатор або токен, а повну адресу підставляє сервер — з поправкою, що перебором таких ідентифікаторів користувач вивчить увесь перелік цілей.
Небезпечна десеріалізація: яке ключове питання до розробника і як пошукати її без доступу до коду?
Ключове питання одне: чи визначає тип обʼєкта сам потік даних. Якщо так, атакувальник вирішує, що саме буде відбудовано, — а в рідних форматах мов, які вміють кастомізувати процес відновлення, це прямий шлях до відмови в обслуговуванні, обходу контролю доступу або віддаленого виконання коду. Перевірка типу після десеріалізації запізніла: у частині типів код виконується просто під час відбудови, тож перевірку ставлять до неї. Аргумент «ми додали фільтр небезпечних класів» слабкий наперед — нові класи-ґаджети знаходять постійно, це той самий deny-list; сильне рішення — не використовувати рідні формати для недовірених даних, а якщо перелік повідомлень відомий, підписувати їх і не відбудовувати без перевіреного підпису. Без доступу до коду найдешевша проба — пошукати сигнатури серіалізованих обʼєктів у перехопленому трафіку: у hex це послідовність AC ED 00 05, у той самий потік починається з rO0, транспорт нерідко видає себе заголовком Content-Type: application/x-java-serialized-object, а закодований pickle упізнається за початком gASV. Знайшли таке в тілі запиту чи в cookie — маєте точку, куди клієнт подає обʼєкт, і про це вже варто говорити з командою.
Три кейси, у яких функціональна перевірка показує зелене, а дефект лишається на місці: поле «підтягнути зображення за посиланням», де стає не картинка, а список адрес, куди сервер відмовився йти; форма аватара, де тікет пишуть не за статусом відповіді, а за наслідком; і XML-, де питання чорної скриньки доводиться перевести в питання про налаштування парсера. Скрізь — що дивитися і чому саме це.
Кейс 1. Поле «імпорт за посиланням»: перевіряємо не результат, а маршрут
Форма приймає URL картинки й показує прев'ю. Функціонально все чудово: даєш посилання на публічний файл — картинка з'являється. Але сама наявність такого поля означає одне: мережевий похід робить сервер, а не браузер. Тому тест будується не навколо «чи підтягнулося», а навколо «куди застосунок відмовився йти» — і негативний набір тут цінніший за позитивний.
| Проба | Здорова реакція | Що означає прийняття |
|---|---|---|
http://169.254.169.254/latest/meta-data/ | відмова до походу | доступ до токенів ролі інстансу — найдорожча знахідка класу |
http://127.0.0.1:8080/admin | відмова | внутрішній сервіс відкрито руками застосунку |
http://[::1]:8080/admin | відмова | фільтр знає IPv4 і не знає IPv6 |
file:///etc/passwd | відмова | перевіряється адреса, але не схема |
дозволений хост, що відповідає 302 на внутрішню адресу | відмова | застосунок слідує редиректам повз пройдену валідацію |
| імʼя, яке резолвиться у внутрішню IP | відмова | валідували рядок, а не результат резолву |
Останні два рядки — те, через що падає більшість «ми ж валідуємо URL»: перевірка одноразова, а між нею й запитом стоять DNS і редирект.
import { test, expect } from '@playwright/test';
// Тільки свій стенд: це проби всередину інфраструктури, а не «клік по кнопці».
const forbidden = [
'http://169.254.169.254/latest/meta-data/', // метадані інстансу
'http://127.0.0.1:8080/admin', // сервіс на самій машині
'http://[::1]:8080/admin', // те саме в IPv6
'http://10.0.0.5/internal', // приватний діапазон
'file:///etc/passwd', // інша схема
];
for (const source of forbidden) {
test(`імпорт за посиланням відхиляє ${source}`, async ({ request }) => {
const res = await request.post('/api/images/import', { data: { source } });
expect(res.status(), 'адресу треба відсіяти ДО мережевого походу').toBe(400);
// у відповіді не має бути слідів того, що сервер таки сходив
expect(await res.text()).not.toMatch(/AccessKeyId|root:x:|meta-data/i);
});
}
Редирект перевіряється окремим тестом, бо це інший дефект: валідація спрацювала, а захист усе одно не втримав.
test('редирект із дозволеного хоста не проводить запит усередину', async ({ request }) => {
// підконтрольний хост, який віддає 302 на адресу метаданих
const hop = 'https://probe.tester.example/r?to=http://169.254.169.254/latest/meta-data/';
const res = await request.post('/api/images/import', { data: { source: hop } });
expect(res.status(), 'слідування редиректам має бути вимкнене').toBe(400);
});
Що дивитися і чому:
- Оракул — статус , а не текст помилки. Якщо на внутрішню адресу приходить
500замість400, це вже сигнал: застосунок пішов і зламався по дорозі, тобто перевірки на маршруті не було. - IPv6 і
file://тримайте в наборі завжди. Вимога до фільтра охоплює IPv4 і IPv6 разом, а перевірка схеми — окремий крок від перевірки хоста: набір, зібраний лише під10.*і192.168.*, обидва пункти пропускає. - Порожня відповідь без помилки — не «безпечно». Сервер міг сходити й просто не показати результат; тоді доказом стає підконтрольний хост у пробі — прийшов на нього запит чи ні.
- Тікет пишіть із першопричиною. Формулювання «приймається повний URL від користувача, редиректи не вимкнено» коштує більше, ніж «SSRF на імпорті картинок»: воно вже містить рішення.
Кейс 2. Форма аватара: оракул — наслідок, а не статус 200
Тестувальник заливає .exe, отримує відмову й пише «завантаження перевірено». Насправді перевірено один вимір із шести, і не найцікавіший. Форма приймає три речі одночасно — вміст, імʼя і заявлений тип, — і кожна з них є окремим недовіреним вводом.
| Що підміняємо | Проба | Здорова реакція | Симптом дефекту |
|---|---|---|---|
| Розширення | avatar.jpg.php | відмова | файл лежить усередині webroot і виконується при відкритті |
| Обрив імені | avatar.php%00.jpg | відмова | на диск лягло .php |
| Заявлений тип | скрипт із Content-Type: image/png | відмова | вердикт виніс заголовок від клієнта |
| Імʼя як шлях | ../../index.html | згенероване імʼя або відмова | перезапис чужого файлу |
| Розмір після розпакування | архів 500 КБ, що дає 5 ГБ | відмова (або ZIP не приймається) | ліміт стоїть на архіві, а не на вмісті |
| Доступ | завантаження без токена і в чужий профіль | 401 / 403 | створення файлу без прав |
import { test, expect } from '@playwright/test';
// Подвійне розширення + брехливий Content-Type в одній пробі:
// саме так виглядає реальна спроба, а не «залив .exe».
test('аватар: виконуваний скрипт під виглядом картинки не приймається', async ({ request }) => {
const res = await request.post('/api/profile/avatar', {
multipart: {
file: {
name: 'avatar.jpg.php',
mimeType: 'image/jpeg',
buffer: Buffer.from('<?php echo "marker-7f3a"; ?>'),
},
},
});
expect(res.status(), 'фільтр має дивитися на останнє розширення').toBe(415);
});
Позитивний сценарій перевіряє не менш важливу річ — що на диску опинилося імʼя від сервера, а не від користувача:
test('імʼя на диску генерує застосунок, а не клієнт', async ({ request }) => {
const res = await request.post('/api/profile/avatar', {
multipart: {
file: { name: '../../index.html', mimeType: 'image/png', buffer: pngFixture },
},
});
expect(res.ok()).toBeTruthy();
const body = await res.json();
expect(body.url, 'перехід у батьківський каталог не має доїжджати до шляху').not.toContain('..');
// очікуємо згенерований ідентифікатор, а не користувацьке імʼя
expect(body.url).toMatch(/\/[0-9a-f-]{36}\.png$/);
});
А якщо файл усе-таки прийнявся — тест не закінчується на 201. Наслідок перевіряють окремим кроком: піти за виданою адресою й подивитися, чим її віддає сервер.
test('завантажений файл віддається як дані, а не виконується', async ({ request }) => {
const uploaded = await request.get(publicUrlOfUploadedFile);
const type = uploaded.headers()['content-type'] ?? '';
expect(type).toMatch(/^image\//);
// код має приїхати дослівно: бачимо сам тег, а не результат його виконання
const body = await uploaded.text();
expect(body, 'відсутність тега означала б, що PHP виконався на сервері').toContain('<?php');
});
Що дивитися і чому:
201— це не пройдений тест, а половина сценарію. Питання завжди друге: що сталося з файлом далі — чи виконався, чи перезаписав чужий, чи прилетить іншому користувачеві разом з активним вмістом.- Маркер у вмісті економить годину. Унікальний рядок усередині завантаженого файлу дає однозначну відповідь, чи його інтерпретували, — але розрізняє випадки не сам маркер, а обгортка навколо нього: приїхав
<?php echo "marker-7f3a"; ?>дослівно — файл віддається як дані; приїхав голийmarker-7f3aбез тега — код виконався, і це критичний тікет. - Перевіряйте два рівні доступу окремо. «Незалогінений не може завантажити» і «залогінений не може писати в чужий профіль» — різні перевірки, і друга провалюється частіше.
- ZIP тестуйте вмістом, а не розміром. Ліміт на завантажений архів бомба проходить чесно; вимірювати треба те, що вийде після розпакування.
Кейс 3. XML-ендпоінт: перевести чорну скриньку в питання про конфігурацію
Інтеграційний ендпоінт приймає XML від партнера. Проба із зовнішньою сутністю нічого не показала — відповідь звичайна, помилки немає. Спокуса написати «XXE не відтворюється» велика й хибна: якщо парсер приймає зовнішні DTD, у сліпого варіанта видимого результату не буває за побудовою. Тому проб має бути дві — на видимий канал і на out-of-band (OOB).
Проба 1 — видимий канал: вміст файлу очікуємо в даних або в тексті помилки.
<?xml version="1.0"?>
<!DOCTYPE order [
<!ENTITY leak SYSTEM "file:///etc/hostname">
]>
<order><comment>&leak;</comment></order>
Проба 2 — сліпий канал: результату у відповіді не буде, доказом стає звернення до нашого хоста.
<?xml version="1.0"?>
<!DOCTYPE order [
<!ENTITY ping SYSTEM "http://probe-7f3a.tester.example/hit">
]>
<order><comment>&ping;</comment></order>
import { test, expect } from '@playwright/test';
test('XML-ендпоінт не резолвить зовнішні сутності', async ({ request }) => {
const runId = 'probe-7f3a';
const payload = [
'<?xml version="1.0"?>',
'<!DOCTYPE order [',
` <!ENTITY ping SYSTEM "http://${runId}.tester.example/hit">`,
']>',
'<order><comment>&ping;</comment></order>',
].join('\n');
const res = await request.post('/api/orders', {
headers: { 'Content-Type': 'application/xml' },
data: payload,
});
// видимий канал: ні даних, ні витоку через текст помилки
expect(await res.text()).not.toMatch(/root:x:|ip-\d+-\d+-\d+/);
// сліпий канал: на підконтрольний хост не мусить прийти жодного звернення
const hits = await fetchProbeHits(runId);
expect(hits, 'сервер сходив за сутністю — завантаження зовнішніх DTD увімкнене').toHaveLength(0);
});
Обидві проби зелені — це ще не вердикт про систему, а вердикт про один ендпоінт на одному стенді. Остаточну відповідь дає конфігурація, і питати треба про неї:
| Де парситься XML | Типовий дефолт | Що з цього випливає |
|---|---|---|
| Java, більшість XML-парсерів | зовнішні сутності історично увімкнені | «безпечно» лише після перегляду прапорців; FEATURE_SECURE_PROCESSING допомагає від вичерпання ресурсів, але від розкриття сутностей рятує не в кожній реалізації |
| PHP 8.0 і новіші, дефолтний libxml2 | XXE блокується самим парсером | той самий код на старшій версії дає інший вердикт — результат перевірки прив'язуйте до версії рантайму |
| Сторонні бібліотеки: SOAP-клієнти, конвертери документів, обробники офісних форматів | залежить від бібліотеки | «наш парсер налаштований» їх не покриває, кожна перевіряється окремо |
Що дивитися і чому:
- Порожня відповідь — не результат. не має видимого виводу; без
out-of-band-каналу (HTTP-звернення або DNS-резолв на підконтрольне імʼя) ви просто не бачите того, що відбувається. - Текст помилки — окремий канал витоку. Прочитаний файл часто приїжджає не в даних, а всередині повідомлення про збій парсингу — тому ставлять і на тіло помилки, а не лише на успішну відповідь.
- Питання формулюйте як конфігураційне. «Чи вимкнено DOCTYPE, зовнішні сутності, завантаження зовнішніх DTD і XInclude» — це те, на що команда може відповісти й що можна закрити одним рішенням; «чи є в нас XXE» такої відповіді не має.
- Рекурсивні сутності — інший підклас тієї ж конфігурації. Повне вимкнення DTD знімає й читання файлів, і billion laughs; часткові заходи закривають щось одне, тому в тікеті варто просити саме повне вимкнення, якщо бізнес-потреби в DTD немає.
- Тікет із першопричиною. У розборі стоїть перед : конфігурація парсера — причина, вихідний запит руками сервера — наслідок. Описаний у цьому порядку тікет не розсипається на два незалежні.
SSRF: механізм і місце в мапі ризиків
- Можу пояснити через того, хто робить запит: адресу дає ввід, а мережевий похід виконує сервер — і успадковує його місце в периметрі, тож контроль на кшталт файрвола просто не спрацьовує.
- Знаю типові цілі поіменно: внутрішні адмінпанелі, сервіси на
localhost, метаданих інстансу (169.254.169.254,metadata.amazonaws.com) — звідти тягнуть облікові дані й токени ролі, а IMDSv2 знімає лише частину сценаріїв. - Називаю категорію разом із роком редакції: власна A10 у 2021, а в 2025 SSRF усередині A01 Broken Access Control — і памʼятаю ordinality
Resultant, тобто в розборі інциденту SSRF стоїть після першопричини.
Захист від SSRF
- Розумію критерій вибору: перелік цілей відомий — , невідомий (вебхуки) — із визнаною межею «непроникною стіною не буває», і мінімальні вимоги до нього — приватні діапазони,
localhost, link-local, однаково в IPv4 і IPv6. - Тримаю в голові три вимоги рівня застосунку, на яких найчастіше й сиплються: рядок URL цілком від користувача не беруть, автоматичний перехід за редиректом вимикають, а резолв імені рахують окремим вектором (легітимний домен, наведений на внутрішню IP).
- Памʼятаю про другий рівень і межу автоматики: мережа лишає застосунку тільки дозволені маршрути, а чистий звіт відсутності класу не доводить — інструмент знаходить лише частину входжень.
Завантаження файлів
- Знаю, що тут є наслідок, а не статус відповіді: виконання завантаженого, перезапис чужого файлу, XSS в іншого користувача — а не «прийнялося з
200». - Можу назвати два класичні обходи перевірки розширення — подвійне
.jpg.phpі нульовий байт у.php%00.jpg, — і пояснити, чому заявленийContent-Typeдоказом типу не є: заголовок формує клієнт, тож працює лише allow-list дозволених типів. - Знаю, що імʼя на диску генерує застосунок (випадковий рядок на кшталт UUID), файли лежать на окремому хості або принаймні поза webroot, а обмеження обсягу застосовують до вмісту після розпакування: архів на кількасот кілобайт проходить ліміт чесно й розгортається в гігабайти вже за ним.
Path traversal
- Можу означити слабкість через межу (шлях із зовнішнього вводу резолвиться поза дозволеною директорією, для вебсервера це web document root) і не роблю висновку з відсутності крапок: є абсолютна форма, а записи
%2e%2e%2fта..%2fрозкриваються в той самий перехід угору. - Розумію, чому вирізання символів не працює:
.../...//складається назад у../після одноразового видалення, а фільтр на/не бачить\— правильний шлях це канонізація штатним засобом (realpath()) із подальшою звіркою, що результат усередині дозволеної директорії. - Памʼятаю дві межі методики: негативний результат без знання платформи нічого не доводить (просити
/etc/passwdв IIS немає сенсу), а частина дефектів без коду не знаходиться взагалі — окремо тримаю в голові traversal другого порядку, який спрацьовує пізніше з бази.
XXE
- Ставлю питання як конфігураційне, бо DTD із зовнішньою сутністю — штатна можливість формату, залишена ввімкненою; тому вердикт привʼязую до рантайму: у Java зовнішні сутності історично доступні за замовчуванням, PHP 8.0+ на libxml2 блокує сам, а сторонні бібліотеки перевіряються окремо.
- Знаю три наслідки — читання локальних файлів (витік часто через текст повідомлення про помилку), вихідні запити руками застосунку, в обслуговуванні — і що повне вимкнення DTD закриває заразом billion laughs.
- Розумію, чому порожня відповідь не є вердиктом: коли парсер тягне зовнішні DTD, реалізується сліпий варіант , у якому у видимій відповіді не буде нічого за побудовою — тож оракул переїжджає в
out-of-band-канал (OOB), а в мапі 2025 сам клас лежить в A02 Security Misconfiguration.
Валідація на сервері
- Знаю різницю allow-list і deny-list і чому фільтр «небезпечних символів» поганий з обох боків: обходиться іншим записом і ламає легітимні дані (
O'Brian), тож deny-list лишається додатковим шаром, а не заміною. - Памʼятаю обовʼязковий порядок кроків (декодування й канонізація, потім валідація, без подвійного декодування) і те, що перевірка живе на сервері, бо клієнтську знімають вимкненим JS чи .
- Можу пояснити, чому значення, якого немає у випадному списку чи серед радіокнопок, сервер має не просто відхилити, а залогувати як подію високої критичності: це ознака втручання в клієнтський код, а не помилки користувача.
Сусідні класи
- Тримаю чітку межу «відкритий редирект vs SSRF»: у першому кудись відводять користувача, який довіряє знайомому імені сервера, у другому мережею йде сама інфраструктура; збігається в них тільки задача валідації URL.
- Знаю ключове питання до десеріалізації — чи задає тип обʼєкта сам потік даних — і що перевірку типу ставлять до відбудови обʼєкта; для чорної скриньки памʼятаю сигнатури в трафіку:
AC ED 00 05, префіксrO0,gASVдля pickle.
Квіз
Перед стартом
- Питань: 17
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
SSRF (Server-Side Request Forgery) — що це?