Пошук уроків, статей та іншого контенту
Використаєте Subresource Integrity і заголовки безпеки для контролю сторонніх скриптів та ресурсів.
Сторонні ресурси з CDN зручні, але створюють додатковий ризик. Якщо CDN буде скомпрометовано або файл за тією самою URL-адресою зміниться, браузер може завантажити вже шкідливий JavaScript.
Subresource Integrity (SRI) дає змогу перевірити, що завантажений ресурс має очікуваний криптографічний хеш.
Заголовки безпеки доповнюють SRI:
Content-Security-Policy (CSP) визначає, звідки дозволено завантажувати скрипти, стилі, зображення та інші ресурси;
X-Content-Type-Options забороняє браузеру вгадувати MIME-тип;
Referrer-Policy обмежує інформацію, яку браузер передає в заголовку Referer;
Permissions-Policy обмежує доступ до можливостей браузера;
Strict-Transport-Security (HSTS) змушує використовувати HTTPS;
frame-ancestors у CSP захищає сторінку від вбудовування у фрейм.
SRI і CSP вирішують різні задачі:
SRI відповідає на питання: «Чи є файл саме тим файлом, який ми очікуємо?»
CSP відповідає на питання: «Чи дозволено цій сторінці завантажувати або виконувати цей ресурс?»
Для захисту стороннього скрипту бажано використовувати обидва механізми.
integrityДля зовнішнього скрипту хеш задається в атрибуті integrity:
<script
src="https://cdn.example.com/library-1.4.2.min.js"
integrity="sha384-BASE64_HASH"
crossorigin="anonymous">
</script>Браузер:
завантажує ресурс;
обчислює його хеш;
порівнює хеш із значенням integrity;
виконує скрипт лише за успішної перевірки.
Якщо хеш не збігається, ресурс не буде виконано.
SRI також можна застосовувати до таблиць стилів:
<link
rel="stylesheet"
href="https://cdn.example.com/theme-2.1.0.css"
integrity="sha384-BASE64_HASH"
crossorigin="anonymous">integrityЗначення складається з алгоритму хешування, символу - і Base64-представлення хешу:
sha256-значення_хешу
sha384-значення_хешу
sha512-значення_хешуДля нових інтеграцій зазвичай використовують sha384 або sha512.
Можна вказати кілька хешів:
<script
src="/assets/app.js"
integrity="sha256-HASH1 sha384-HASH2">
</script>Браузер може використати один із підтримуваних хешів. Не слід без потреби додавати багато варіантів: це ускладнює аудит і оновлення.
Хеш потрібно обчислювати для точних байтів файлу. Навіть один пробіл, зміна переносу рядка або інша версія мініфікатора змінює хеш.
Наприклад, за допомогою OpenSSL:
openssl dgst -sha384 -binary library-1.4.2.min.js \
| openssl base64 -AПрефікс sha384- додається вручну:
sha384-результат-командиАбо весь рядок можна отримати так:
printf 'sha384-' && \
openssl dgst -sha384 -binary library-1.4.2.min.js \
| openssl base64 -AВажливо перевіряти хеш після того, як файл остаточно завантажено. Не можна обчислювати його для одного файлу, а потім підміняти файл іншим під час публікації.
Для cross-origin-ресурсу потрібен коректний CORS-відповідь сервера. Наприклад, CDN має повернути:
Access-Control-Allow-Origin: https://app.example.comабо, якщо це припустимо для ресурсу:
Access-Control-Allow-Origin: *У HTML зазвичай вказують:
<script
src="https://cdn.example.com/library-1.4.2.min.js"
integrity="sha384-BASE64_HASH"
crossorigin="anonymous">
</script>Значення crossorigin="anonymous" означає, що запит не містить облікових даних, таких як cookies або HTTP-авторизація.
Якщо CDN не підтримує CORS, браузер не зможе використати SRI для такого cross-origin-ресурсу.
Для same-origin-ресурсів атрибут crossorigin зазвичай не потрібен:
<script
src="/assets/app.7f31c.js"
integrity="sha384-BASE64_HASH">
</script>Не використовуйте URL на кшталт:
https://cdn.example.com/library/latest.jsТакий файл може змінитися без зміни URL. Після цього:
SRI заблокує новий файл, якщо хеш залишився старим;
без SRI браузер непомітно виконає новий код.
Краще використовувати версію в URL:
<script
src="https://cdn.example.com/library/1.4.2/library.min.js"
integrity="sha384-BASE64_HASH"
crossorigin="anonymous">
</script>Під час оновлення потрібно:
отримати нову версію;
перевірити її походження;
обчислити новий хеш;
оновити URL і integrity одночасно;
протестувати сторінку;
переглянути зміни у коді залежності.
SRI не додається автоматично до скриптів, які створюються через JavaScript:
const script = document.createElement("script");
script.src = "https://cdn.example.com/library.js";
document.head.append(script);Для такого завантаження потрібно явно встановити integrity:
const script = document.createElement("script");
script.src = "https://cdn.example.com/library-1.4.2.min.js";
script.integrity = "sha384-BASE64_HASH";
script.crossOrigin = "anonymous";
document.head.append(script);Однак для критичних залежностей краще використовувати статичний HTML або збирати залежності у власний пакет. Динамічні URL складніше перевіряти під час аудиту.
CSP задається HTTP-заголовком:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'Мінімальна політика:
дозволяє ресурси з поточного походження через default-src 'self';
дозволяє JavaScript лише з поточного походження;
повністю забороняє плагіни через object-src 'none';
забороняє зміну базового URL документа через base-uri 'none'.
Приклад більш повної політики:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self';
img-src 'self' https: data:;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requestsПризначення директив:
default-src — значення за замовчуванням для більшості типів ресурсів;
script-src — джерела JavaScript;
style-src — джерела CSS;
img-src — зображення;
font-src — вебшрифти;
connect-src — fetch, XMLHttpRequest, WebSocket та інші мережеві з’єднання;
object-src — застарілі плагіни та елементи <object>;
base-uri — допустимі значення для <base>;
frame-ancestors — хто може вбудовувати сторінку у frame;
form-action — адреси надсилання HTML-форм;
upgrade-insecure-requests — перетворення HTTP-URL на HTTPS.
frame-ancestors ефективніший за старий заголовок X-Frame-Options, оскільки дозволяє детальніше керувати політикою. Для сумісності зі старими браузерами іноді додають обидва:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYРозглянемо ресурс:
<script
src="https://cdn.example.com/library-1.4.2.min.js"
integrity="sha384-BASE64_HASH"
crossorigin="anonymous">
</script>Для нього CSP має дозволити походження CDN:
Content-Security-Policy: script-src 'self' https://cdn.example.comCSP не перевіряє автоматично, що зовнішній файл має потрібний хеш. Цю перевірку виконує SRI.
Тому безпека залежить від двох умов:
https://cdn.example.com дозволено політикою CSP;
фактичний файл відповідає integrity.
Якщо CDN не має бути джерелом скриптів, його не потрібно додавати до script-src, навіть якщо в HTML присутній SRI.
Сувора CSP не повинна дозволяти:
script-src 'unsafe-inline'Ця директива дозволяє виконання довільних inline-скриптів і суттєво послаблює захист від XSS.
Для inline-скрипту можна використати nonce:
Content-Security-Policy: script-src 'self' 'nonce-random-value'<script nonce="random-value">
console.log("Дозволений скрипт");
</script>Nonce має бути:
криптографічно випадковим;
достатньо довгим;
новим для кожної відповіді;
недоступним для непередбачених користувацьких даних.
Не можна використовувати один nonce для всіх сторінок або зберігати його як константу в коді.
Для статичного inline-скрипту можна використати CSP-хеш:
Content-Security-Policy: script-src 'self' 'sha256-BASE64_HASH'Хеш у CSP обчислюється для точного тексту inline-скрипту, включно з пробілами та переносами рядків.
CSP-хеш і SRI-хеш мають подібний формат, але це різні механізми:
SRI перевіряє зовнішній ресурс;
CSP-хеш дозволяє конкретний inline-код.
strict-dynamicДля застосунків, які завантажують скрипти через довірений bootstrap-скрипт, можна використовувати nonce або хеш разом із strict-dynamic:
Content-Security-Policy:
script-src 'nonce-random-value' 'strict-dynamic';
object-src 'none';
base-uri 'none'strict-dynamic передає довіру скриптам, які були завантажені скриптом із правильним nonce або хешем. Це зручно для складних застосунків, але вимагає чіткого контролю над тим, які URL створює довірений код.
Не слід додавати strict-dynamic без розуміння поведінки браузерів і графа залежностей застосунку.
X-Content-Type-OptionsX-Content-Type-Options: nosniffБраузер не повинен намагатися вгадувати тип ресурсу, якщо сервер вказав Content-Type.
Сервер також має повертати правильні MIME-типи:
Content-Type: application/javascript
Content-Type: text/cssReferrer-PolicyReferrer-Policy: strict-origin-when-cross-originДля same-origin-запитів браузер може передавати повний URL, а для cross-origin — лише origin. Це зменшує ризик витоку приватних параметрів із URL.
Для суворішої політики можна використати:
Referrer-Policy: no-referrerPermissions-PolicyPermissions-Policy:
camera=(),
microphone=(),
geolocation=(),
payment=()Така політика забороняє сторінці та її фреймам використовувати камеру, мікрофон, геолокацію і платіжні API.
Дозволи потрібно обмежувати відповідно до функцій застосунку. Не варто без потреби залишати широкі дозволи.
Strict-Transport-Security: max-age=31536000; includeSubDomainsHSTS наказує браузеру використовувати HTTPS протягом вказаного часу.
Не надсилайте цей заголовок через звичайний HTTP у локальному середовищі. На production він має надходити через HTTPS.
Параметр includeSubDomains потрібно використовувати лише тоді, коли всі піддомени підтримують HTTPS. Інакше окремий піддомен може стати недоступним.
Нижче наведено невеликий Node.js-сервер без сторонніх пакетів. Він:
створює JavaScript-файл;
обчислює для нього SRI-хеш;
додає хеш до HTML;
надсилає CSP та інші заголовки;
віддає сторінку лише з same-origin-скриптом.
Створіть файл server.mjs:
import http from "node:http";
import crypto from "node:crypto";
const port = 3000;
const appJavaScript = `
const button = document.querySelector("#load");
const output = document.querySelector("#output");
button.addEventListener("click", () => {
output.textContent = "Скрипт виконано після успішної перевірки SRI.";
});
`.trim();
const integrity = `sha384-${crypto
.createHash("sha384")
.update(appJavaScript, "utf8")
.digest("base64")}`;
const html = `<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SRI example</title>
<script src="/app.js" integrity="${integrity}"></script>
</head>
<body>
<h1>Перевірка SRI</h1>
<button id="load" type="button">Виконати скрипт</button>
<p id="output">Скрипт ще не виконувався.</p>
</body>
</html>`;
const securityHeaders = {
"Content-Security-Policy": [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
"connect-src 'self'"
].join("; "),
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
"X-Frame-Options": "DENY"
};
const server = http.createServer((request, response) => {
Object.entries(securityHeaders).forEach(([name, value]) => {
response.setHeader(name, value);
});
if (request.url === "/") {
response.writeHead(200, {
"Content-Type": "text/html; charset=utf-8"
});
response.end(html);
return;
}
if (request.url === "/app.js") {
response.writeHead(200, {
"Content-Type": "application/javascript; charset=utf-8"
});
response.end(appJavaScript);
return;
}
response.writeHead(404, {
"Content-Type": "text/plain; charset=utf-8"
});
response.end("Not found");
});
server.listen(port, () => {
console.log(`Відкрийте http://localhost:${port}`);
});Запустіть сервер:
node server.mjsВідкрийте в браузері:
http://localhost:3000Після натискання кнопки текст зміниться. Якщо змінити вміст appJavaScript, але не оновити обчислення хешу, сервер у цьому прикладі автоматично перерахує хеш. У реальному застосунку HTML і JavaScript зазвичай збираються окремим build-процесом, а хеш фіксується у згенерованому HTML або маніфесті.
Щоб побачити роботу SRI, можна тимчасово замінити атрибут у HTML:
<script
src="/app.js"
integrity="sha384-invalid-hash">
</script>Браузер заблокує ресурс, а в консолі розробника з’явиться помилка про невідповідність integrity-хешу.
Перед увімкненням суворої політики на production корисно використати:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
object-src 'none';
report-uri /csp-reportУ цьому режимі браузер повідомляє про порушення, але не блокує ресурс.
Сучасні системи також можуть використовувати report-to, але сервер має правильно налаштувати відповідну групу звітування. Незалежно від формату, звіти потрібно:
збирати на окремому endpoint;
фільтрувати від повторів;
не записувати в них зайві персональні дані;
аналізувати до переходу на режим блокування.
Після виправлення легітимних порушень політику можна перевести у звичайний Content-Security-Policy.
Припустімо, застосунок використовує бібліотеку з CDN:
<script
src="https://cdn.example.com/widget/3.2.1/widget.min.js"
integrity="sha384-BASE64_HASH"
crossorigin="anonymous">
</script>Відповідна CSP може мати такий вигляд:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self';
connect-src 'self' https://api.example.com;
img-src 'self' https:;
object-src 'none';
base-uri 'none';
frame-ancestors 'none'Якщо бібліотека також завантажує свої скрипти з інших джерел, це потрібно врахувати окремо. Не слід без аналізу додавати до CSP широкі дозволи:
script-src *або:
script-src 'unsafe-inline' 'unsafe-eval' *Такі правила часто зводять захист CSP нанівець.
Приклад набору заголовків:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self'; img-src 'self' https: data:; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomainsКонкретні джерела потрібно адаптувати до застосунку. Надмірно сувора політика може зламати функціональність, а надто широка — не забезпечити достатнього захисту.
Хеш має бути Base64-представленням бінарного результату SHA-256, SHA-384 або SHA-512. Hex-рядок не є правильним значенням для SRI.
Неправильно:
<script integrity="sha384-9f86d081884c7d659a2feaa0c55ad015">
</script>Правильний хеш повинен мати Base64-формат, наприклад:
<script integrity="sha384-BASE64_VALUE">
</script>Поширені причини:
хеш обчислили до мініфікації;
у production використовується інший build;
CDN повертає іншу версію;
файл був змінений після обчислення хешу;
хешували архів замість JavaScript-файлу.
crossoriginДля cross-origin-ресурсів без crossorigin="anonymous" SRI може не працювати, якщо CDN не надає потрібний CORS-відповідь.
Це правило:
Content-Security-Policy: script-src 'self' https://cdn.example.comдозволяє завантажувати скрипти з CDN, але не гарантує незмінність їхнього вмісту. Додавайте SRI до конкретного ресурсу.
unsafe-inlinescript-src 'self' 'unsafe-inline'Це значно послаблює CSP. Краще перенести код у зовнішні файли або використовувати nonce чи CSP-хеш.
Nonce, записаний у шаблоні як константа, не є захистом. Компрометований або витікший nonce можна повторно використати.
Приклади небезпечних дозволів:
script-src *connect-src *img-src *Особливо небезпечним є script-src *, оскільки він розширює набір джерел виконуваного коду.
includeSubDomains може зробити недоступними піддомени, які ще працюють через HTTP. Спочатку перевірте, що вся потрібна інфраструктура підтримує HTTPS.
SRI захищає від зміни конкретного файлу, але не вирішує всі ризики сторонньої залежності:
залежність може містити небажану логіку вже у перевіреній версії;
бібліотека може завантажувати додаткові ресурси;
CSP може бути налаштована надто широко;
залежність може мати вразливість у власному коді.
Критичні бібліотеки варто регулярно оновлювати, перевіряти та за можливості збирати у власний контрольований bundle.
SRI перевіряє цілісність зовнішніх JavaScript- і CSS-файлів за криптографічним хешем.
Для cross-origin-ресурсів потрібні коректний CORS і часто crossorigin="anonymous".
Версії CDN-залежностей потрібно фіксувати, а не використовувати latest.
CSP обмежує джерела ресурсів і захищає від багатьох сценаріїв XSS.
script-src 'self' або точний список джерел безпечніші за script-src *.
Для inline-коду використовуйте nonce або CSP-хеш замість 'unsafe-inline'.
X-Content-Type-Options, Referrer-Policy, Permissions-Policy, HSTS і frame-ancestors доповнюють захист.
Перед увімкненням суворої CSP корисно використати Content-Security-Policy-Report-Only.
SRI і CSP потрібно застосовувати разом: перший контролює вміст ресурсу, друга — право на його завантаження та виконання.