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

Junior: питання на співбесіді з теми «Заголовки й захист у браузері»

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

5 питань

Content Security Policy (CSP) - заголовок відповіді, яким сервер каже браузеру, звідки дозволено завантажувати й виконувати ресурси на цій сторінці: скрипти, стилі, зображення, шрифти, фрейми, запити fetch.

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; img-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Головна ціль - зменшити наслідки XSS. Навіть якщо нападник зміг вставити <script> у сторінку, браузер його не виконає: скрипт не має дозволеного джерела чи правильного nonce. CSP - другий рубіж: основний захист від XSS - екранування виводу, а CSP спрацьовує, коли екранування десь пропустили.

Основні директиви:

  • default-src - запасне правило для всіх типів ресурсів;
  • script-src, style-src, img-src, connect-src (куди можна робити fetch/WebSocket), font-src, frame-src;
  • object-src 'none' - заборона застарілих плагінів;
  • base-uri 'self' - захист від підміни <base>, через яку відносні посилання на скрипти ведуть на чужий домен;
  • form-action - куди можна відправляти форми;
  • frame-ancestors - хто може вбудовувати сторінку у фрейм (захист від clickjacking).

Що CSP блокує за замовчуванням, щойно задано script-src:

  • інлайнові скрипти (<script>...</script>, onclick="...", javascript: у посиланнях);
  • eval() і new Function() - без 'unsafe-eval'.

Чого CSP не дає:

  • не захищає від CSRF, SQL-ін'єкцій та логічних помилок;
  • 'unsafe-inline' у script-src майже повністю знецінює захист від XSS - тому інлайнові скрипти дозволяють через nonce чи хеш, а не через 'unsafe-inline'.

У Laravel вбудованого middleware для CSP немає: заголовок ставлять власним middleware або пакетом spatie/laravel-csp, а Vite::useCspNonce() додає nonce до тегів, які генерує @vite.

Докладніше в документації: MDN: Content Security Policy

Clickjacking - нападник вбудовує ваш сайт у невидимий <iframe> на своїй сторінці й розміщує його над власною приманкою: «Натисніть, щоб отримати приз». Користувач думає, що клікає по кнопці нападника, а насправді натискає «Видалити акаунт», «Підтвердити переказ» чи «Надати доступ» на вашому сайті - зі своєю активною сесією.

<!-- сторінка нападника -->
<iframe src="https://bank.example/transfer?to=attacker" style="opacity:0; position:absolute; top:0"></iframe>
<button>Отримати подарунок</button>

Захист - заборонити вбудовувати сторінки у фрейми на чужих сайтах.

1. CSP frame-ancestors - сучасний спосіб:

Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self' https://partner.example
  • 'none' - сторінку не можна вбудувати ніде;
  • 'self' - лише на сторінках того самого походження;
  • можна перелічити довірені домени.

2. X-Frame-Options - старий заголовок, ще корисний для старих браузерів:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

ALLOW-FROM застарів і сучасними браузерами не підтримується - для вибіркового дозволу лише frame-ancestors. Якщо задано обидва заголовки, браузери з підтримкою CSP використовують frame-ancestors.

Що варто знати:

  • frame-ancestors не працює в тегу <meta> - лише в HTTP-заголовку;
  • захищати потрібно всі сторінки з діями, а не лише головну: найчастіше атакують налаштування облікового запису, підтвердження платежів, сторінки OAuth-згоди;
  • JavaScript-захист на кшталт «якщо top !== self, перейти на верхній рівень» (frame busting) легко обходиться атрибутом sandbox у фреймі нападника - заголовки надійніші;
  • якщо сайт справді має вбудовуватися (віджети, платіжні форми), - вбудовувати лише окремі сторінки з явним переліком дозволених доменів, а не весь застосунок.

Додатковий бар'єр - cookie сесії з SameSite=Lax чи Strict: у фреймі на чужому сайті браузер не надішле таку cookie, і користувач виявиться неавтентифікованим.

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

Браузери історично вміли «вгадувати» тип вмісту (MIME sniffing): якщо сервер віддав файл як text/plain, а всередині схоже на HTML чи JavaScript, браузер міг обробити його як HTML чи скрипт. Це допомагало погано налаштованим серверам, але відкривало вразливість.

Атака: користувач завантажує на ваш сайт файл, нібито зображення чи текст, але з вмістом-скриптом. Якщо потім цей файл підключають як <script src="/uploads/avatar.jpg"> (наприклад, через XSS, що дозволяє лише посилання на свій домен) чи відкривають напряму, браузер може «впізнати» в ньому HTML або JavaScript і виконати.

X-Content-Type-Options: nosniff вимикає вгадування:

X-Content-Type-Options: nosniff
  • скрипти виконуються лише з JavaScript-типом (text/javascript та подібні) - файл з image/jpeg у <script> буде заблоковано;
  • стилі застосовуються лише з text/css;
  • браузер довіряє заголовку Content-Type, а не вмісту.

Що з цього випливає для бекенду:

  • правильний Content-Type для всього, що віддає сервер: JSON - application/json, JavaScript - text/javascript. З nosniff неправильний тип ламає сайт одразу, тому помилки знаходяться швидко;
  • файли користувачів - з типом, визначеним сервером за вмістом, а не за назвою файлу від користувача. Небезпечні типи (HTML, SVG з можливими скриптами) - з Content-Disposition: attachment, щоб браузер завантажував їх, а не відкривав;
  • окремий домен для користувацького контенту (як githubusercontent.com) - найнадійніший захист: навіть якщо файл виконається, він не матиме доступу до cookie й даних основного сайту.

Як додати в Laravel: власний middleware для групи web або налаштування вебсервера (Nginx/Caddy) для всіх відповідей. Заголовок дешевий і майже не має побічних ефектів - його ставлять завжди, разом з Referrer-Policy, frame-ancestors і CSP.

Докладніше в документації: MDN: X-Content-Type-Options

Коли користувач переходить за посиланням чи сторінка завантажує сторонній ресурс (зображення, скрипт), браузер надсилає заголовок Referer - адресу сторінки, звідки прийшов запит.

Чим це небезпечно: URL часто містить те, що не повинно потрапляти на інші сайти:

  • токени - посилання для скидання пароля (/reset-password?token=...), підтвердження email, запрошення;
  • ідентифікатори й персональні дані - /orders/9921, /patients/42, пошукові запити з іменами;
  • внутрішні адреси адмінок і службових сторінок.

Якщо на сторінці скидання пароля є посилання на зовнішній сайт чи підключено сторонній скрипт аналітики, токен опиниться в їхніх логах.

Referrer-Policy керує, скільки інформації передавати:

Значення Що отримує інший сайт
no-referrer нічого
same-origin повний URL лише для свого походження, іншим - нічого
strict-origin лише походження (https://example.com/), і нічого при переході з HTTPS на HTTP
strict-origin-when-cross-origin повний URL для своїх, лише походження для чужих, нічого при пониженні до HTTP
unsafe-url завжди повний URL - небезпечно
Referrer-Policy: strict-origin-when-cross-origin

strict-origin-when-cross-origin - типове значення сучасних браузерів, якщо політику не задано. Але покладатися на значення за замовчуванням не варто - краще задати явно.

Для чутливих сторінок (скидання пароля, сторінки з токенами в URL) - no-referrer на рівні сторінки чи окремого посилання:

<meta name="referrer" content="no-referrer">
<a href="https://external.example" rel="noreferrer">...</a>

Кращий підхід - не тримати секрети в URL узагалі: токен з посилання одразу обміняти на сесію чи прибрати з адресного рядка після першого використання (history.replaceState чи редирект на чисту адресу).

Аналітика: деякі маркетингові інструменти покладаються на повний Referer. Компроміс - strict-origin-when-cross-origin, а не unsafe-url: для аналізу джерел трафіку зазвичай досить домену.

Докладніше в документації: MDN: Referrer-Policy

Відкритий редирект - сайт перенаправляє на адресу з параметра запиту, не перевіряючи її:

// вразливо
return redirect($request->query('return_to'));
https://bank.example/login?return_to=https://bank-example.attacker.io/login

Посилання веде на справжній домен банку - користувач і поштовий фільтр йому довіряють. Після входу (чи одразу) людину перекидає на фішингову копію, яка просить «повторно ввести пароль» чи дані картки.

Чим ще небезпечно:

  • крадіжка токенів в OAuth: якщо redirect_uri чи проміжний редирект відкритий, код авторизації чи токен може піти на сайт нападника;
  • обхід перевірок «дозволених доменів» в інших системах, що довіряють вашому домену;
  • SSRF, якщо сервер сам переходить за такими редиректами.

Як повертати користувача безпечно:

1. Лише відносні шляхи свого сайту:

$target = $request->query('return_to', '/');

$isLocalPath = str_starts_with($target, '/')
    && ! str_starts_with($target, '//')
    && ! str_contains($target, '\\');

return redirect($isLocalPath ? $target : '/');

//attacker.io - це протокол-відносний URL на чужий домен, а деякі браузери трактують /\attacker.io так само. Тому перевірка лише на початковий / недостатня.

2. Дозволений список - якщо потрібні переходи на інші домени (піддомени, партнери): порівнювати розібраний хост (parse_url) з переліком, а не шукати підрядок (str_contains($url, 'example.com') пропустить example.com.attacker.io).

3. Не передавати URL у параметрі взагалі: зберігати ціль у сесії. Саме так працює Laravel: middleware auth запам'ятовує запитану сторінку (url.intended), а після входу redirect()->intended('/dashboard') повертає туди - URL не проходить через параметр, який може змінити нападник.

4. Ідентифікатори замість адрес: ?next=orders з мапою на маршрути замість довільного URL.

Проміжна сторінка «Ви переходите на зовнішній сайт ...» - компроміс, якщо зовнішні переходи справді потрібні (наприклад, посилання в повідомленнях користувачів).

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