Senior: питання на співбесіді з теми «Заголовки й захист у браузері»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
DOM XSS виникає повністю в браузері: JavaScript бере дані з контрольованого нападником джерела (location.hash, location.search, postMessage, відповідь API) і передає в небезпечний приймач (sink):
element.innerHTML = location.hash.slice(1); // HTML-приймач
script.src = params.get('widget'); // URL скрипта
setTimeout(userInput); // рядок як код
document.write(...); eval(...); new Function(...);
Сервер такої вразливості не бачить: шкідливе значення взагалі може не доходити до нього (фрагмент #... не надсилається на сервер). Знайти всі такі місця у великому застосунку й бібліотеках - важко.
Trusted Types змінюють правила гри: з увімкненою політикою небезпечні приймачі відмовляються приймати звичайні рядки. Їм потрібні спеціальні типізовані об'єкти (TrustedHTML, TrustedScript, TrustedScriptURL), які можна створити лише через зареєстровані політики.
Увімкнення - через CSP:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types app-sanitizer
Політика - єдине місце, де рядок стає «довіреним»:
const sanitizer = trustedTypes.createPolicy('app-sanitizer', {
createHTML: (input) => DOMPurify.sanitize(input),
});
element.innerHTML = sanitizer.createHTML(userHtml); // дозволено
element.innerHTML = userHtml; // TypeError - заблоковано
Що це дає:
- кількість місць для перевірки зменшується до кількох політик замість сотень присвоєнь
innerHTMLпо всьому коду; - порушення помітні: спроба записати рядок у приймач кидає помилку (і може звітувати через CSP);
- контроль над бібліотеками: директива
trusted-typesобмежує, які політики можна створювати, - стороння бібліотека не створить «свою» лазівку непомітно.
Впровадження:
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'- зібрати всі місця, де рядки потрапляють у приймачі;- переписати їх на безпечні API (
textContent,setAttributeдля безпечних атрибутів) або пропустити через політику з санітизацією; - увімкнути блокування.
Обмеження:
- підтримка браузерами: Trusted Types давно є в Chromium, інші браузери додавали її пізніше - перед покладанням на них як на основний захист варто перевірити актуальну підтримку. Там, де Trusted Types немає, код має лишатися безпечним сам по собі;
- фреймворки (React, Vue) мають власний захист від XSS у шаблонах; Trusted Types закривають «аварійні виходи» на кшталт
dangerouslySetInnerHTML,v-htmlі прямої роботи з DOM.
postMessage - легальний спосіб обміну даними між вікнами різних походжень: сторінка й вбудований <iframe> платіжної форми, вікно OAuth-входу й основний застосунок, віджет на чужому сайті й ваш сервер.
// відправник
iframe.contentWindow.postMessage({ type: 'resize', height: 640 }, 'https://widget.example');
// отримувач
window.addEventListener('message', (event) => {
if (event.origin !== 'https://app.example') return; // обов'язкова перевірка
if (event.data?.type === 'resize') {
iframe.style.height = `${Number(event.data.height)}px`;
}
});
Дві обов'язкові перевірки:
1. При відправленні - конкретне targetOrigin, а не '*'.
popup.postMessage({ token }, '*'); // небезпечно
Якщо вікно встигли перенаправити на інший сайт (наприклад, фрейм завантажив сторінку нападника), повідомлення з токеном отримає чужий код. З конкретним targetOrigin браузер просто не доставить повідомлення не тому адресату.
2. При отриманні - перевірка event.origin. Будь-яка сторінка, що має посилання на ваше вікно (відкрила його через window.open чи вбудувала у фрейм), може надіслати йому повідомлення. Без перевірки обробник виконає команди нападника.
Типові помилки:
- перевірка походження за підрядком:
event.origin.includes('example.com')пропуститьhttps://example.com.attacker.io. Лише точне порівняння з переліком; - довіра вмісту від довіреного походження: навіть повідомлення від свого фрейму - це дані, які могли прийти від користувача. Небезпечно передавати їх у
innerHTML,eval,location.href(схемаjavascript:); event.sourceбез перевірки - відповідати варто лише наevent.source, а не на «будь-яке вікно»;- чутливі дані в повідомленнях без потреби - кожне повідомлення може побачити розширення браузера чи інший слухач на тій самій сторінці;
- слухачі, що лишилися після закриття фрейму, - накопичуються в SPA.
Структура повідомлень: тип і версія в кожному повідомленні, валідація схеми (як для API). Повідомлення - це міжсайтовий API, і до нього ті самі вимоги.
Альтернатива для однієї вкладки свого ж походження - BroadcastChannel, а для запитів між вікнами з відповіддю - MessageChannel з окремими портами: менше слухачів на глобальному window.
Після атак класу Spectre стало зрозуміло, що код на сторінці може прочитати дані з пам'яті процесу браузера через побічні канали (точні таймери, спільна пам'ять). Тому браузери ізолюють сайти в окремих процесах, а потужні API (SharedArrayBuffer, таймери високої точності) доступні лише сторінкам у режимі крос-доменної ізоляції.
Три заголовки:
COOP - Cross-Origin-Opener-Policy: розриває зв'язок між вашим вікном і вікнами інших походжень, відкритими через window.open чи посилання.
Cross-Origin-Opener-Policy: same-origin
Чужа сторінка, що відкрила ваш сайт, не отримає посилання на ваше вікно (window.opener = null) - і не зможе ним маніпулювати (атаки на кшталт XS-Leaks, підміна вкладки). same-origin-allow-popups - компроміс для сайтів, яким потрібні спливаючі вікна OAuth чи платежів.
COEP - Cross-Origin-Embedder-Policy: сторінка завантажує сторонні ресурси лише якщо вони явно дозволили вбудовування.
Cross-Origin-Embedder-Policy: require-corp
credentialless - м'якший варіант: сторонні ресурси без дозволу завантажуються, але без cookie.
CORP - Cross-Origin-Resource-Policy: ставить власник ресурсу - хто може його вбудовувати.
Cross-Origin-Resource-Policy: same-origin # лише свій сайт
Cross-Origin-Resource-Policy: same-site
Cross-Origin-Resource-Policy: cross-origin # будь-хто (для публічного CDN)
Крос-доменна ізоляція = COOP: same-origin + COEP: require-corp (чи credentialless). Тоді window.crossOriginIsolated === true, і доступні SharedArrayBuffer, точні таймери, performance.measureUserAgentSpecificMemory().
Кому це потрібно:
- застосункам на WebAssembly з потоками (відео- й аудіоредактори, ігри, CAD), обчисленням у браузері;
- CORP і COOP корисні всім як захист від витоків між сайтами, навіть без повної ізоляції.
Ціна COEP: кожен сторонній ресурс (зображення з CDN, шрифти, рекламні й аналітичні скрипти, фрейми) має надсилати CORP чи CORS-заголовки - інакше перестане завантажуватися. Для сайтів з великою кількістю стороннього вмісту повна ізоляція часто нереальна без credentialless.
Практичний мінімум для звичайного сайту: COOP: same-origin (чи same-origin-allow-popups) і CORP: same-origin для приватних ресурсів (файли користувачів, API) - дешевий захист від частини витоків між вкладками.
Скрипт, підключений на сторінку через <script src>, має ті самі права, що й ваш код: бачить DOM, введення в поля форм (включно з паролями й картками), робить запити від імені користувача, читає localStorage. Аналітика, пікселі реклами, чат підтримки, A/B-тести - кожен сторонній скрипт - це довіра до постачальника й усього його ланцюжка постачання.
Реальні сценарії атак:
- компрометація постачальника - скрипт аналітики чи віджета на CDN підмінили, і він крав дані платіжних форм на тисячах сайтів (атаки класу Magecart);
- тег-менеджер (Google Tag Manager тощо) - маркетологи додають скрипти без рев'ю розробників. Обліковий запис тег-менеджера стає способом виконати довільний код на сайті;
- прострочений домен: скрипт підключено з домену, який постачальник перестав продовжувати, - його реєструє нападник.
Як зменшити ризик:
1. Мінімізація: прибирати скрипти, якими ніхто не користується. Не підключати сторонній код на найчутливіших сторінках (оплата, вхід, налаштування безпеки).
2. CSP: явний перелік дозволених джерел, connect-src - щоб навіть скомпрометований скрипт не міг відправити дані на довільний домен.
3. SRI для незмінних сторонніх файлів із фіксованою версією.
4. Власна копія (self-hosting) - бібліотеки з npm у своєму бандлі замість CDN.
5. Ізоляція в <iframe> з sandbox - сторонній віджет працює в окремому походженні й не має доступу до вашої сторінки:
<iframe
src="https://widget.example/embed"
sandbox="allow-scripts allow-forms"
referrerpolicy="no-referrer"
></iframe>
sandbox без значень забороняє все: скрипти, форми, спливаючі вікна, навігацію верхнього вікна. Дозволи додаються по одному.
Головна пастка sandbox: allow-scripts разом із allow-same-origin для вмісту з вашого ж походження знецінює пісочницю: скрипт усередині може дістатися до батьківської сторінки й прибрати атрибут sandbox у свого фрейму. Користувацький HTML варто віддавати з окремого домену (як usercontent.example) і в пісочниці.
6. Процес: перелік сторонніх скриптів з власником і метою, рев'ю перед додаванням у тег-менеджер, двофакторна автентифікація й мінімальні права для облікових записів тег-менеджера.
Регуляторний аспект: PCI DSS 4.0 вимагає інвентаризації й контролю цілісності всіх скриптів на сторінках оплати.
Докладніше в документації: OWASP: керування сторонніми JavaScript