Пошук уроків, статей та іншого контенту
Захистите сторінки від clickjacking за допомогою CSP frame-ancestors і заголовка X-Frame-Options.
Clickjacking — це атака, під час якої зловмисник вбудовує справжню сторінку у iframe, робить її невидимою або візуально маскує, а користувача змушує натискати на елементи, думаючи, що він взаємодіє з іншою сторінкою.
Наприклад, користувач бачить кнопку «Отримати бонус», але насправді натискає на кнопку «Видалити обліковий запис» у прозорому iframe.
<!-- Сторінка зловмисника -->
<style>
iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
z-index: 2;
}
.fake-button {
position: relative;
z-index: 1;
margin: 200px;
padding: 20px;
background: green;
color: white;
}
</style>
<div class="fake-button">Отримати бонус</div>
<iframe src="https://example.com/account/settings"></iframe>Якщо сторінка example.com дозволяє вбудовування, користувач може виконати небажану дію, не розуміючи, що саме натиснув.
Історично використовували так званий frame busting — JavaScript, який перевіряє, чи сторінка відкривається у верхньому вікні:
if (window.top !== window.self) {
window.top.location = window.self.location;
}Такий підхід не варто використовувати як основний захист:
JavaScript може бути вимкнений;
поведінку скрипта можна заблокувати або обійти;
сторінка може завантажуватися до виконання скрипта;
політика безпеки браузера може заборонити доступ до window.top;
захист залежить від клієнтського коду, а не від політики браузера.
Надійний захист налаштовують за допомогою HTTP-заголовків:
Content-Security-Policy з директивою frame-ancestors;
X-Frame-Options як сумісний резервний механізм.
frame-ancestorsЗаголовок Content Security Policy можна використовувати, щоб вказати, які сайти мають право вбудовувати сторінку у iframe, frame, object або embed.
Заборонити будь-яке вбудовування:
Content-Security-Policy: frame-ancestors 'none'Дозволити вбудовування лише сторінками з того самого джерела:
Content-Security-Policy: frame-ancestors 'self'Дозволити конкретні джерела:
Content-Security-Policy: frame-ancestors 'self' https://admin.example.com https://partner.exampleУ цьому випадку сторінку можна вбудовувати:
самою сторінкою через інше вікно того самого джерела;
з https://admin.example.com;
з https://partner.example.
frame-ancestorsДжерело визначається схемою, хостом і портом. Наприклад, ці адреси не є одним і тим самим джерелом:
https://example.com
http://example.com
https://www.example.com
https://example.com:8443Тому дозвіл для https://example.com не означає автоматичний дозвіл для http://example.com або https://www.example.com.
Сторінки з такими функціями зазвичай не повинні вбудовуватися взагалі:
профіль користувача;
налаштування безпеки;
зміна пароля;
платіжні операції;
адміністративні дії;
видалення даних.
Для них використовуйте:
Content-Security-Policy: frame-ancestors 'none'Приклад відповіді сервера:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Security-Policy: frame-ancestors 'none'
<!doctype html>
<html lang="uk">
<body>
<h1>Налаштування облікового запису</h1>
</body>
</html>Браузер не дозволить іншому сайту відобразити таку сторінку всередині iframe.
X-Frame-OptionsX-Frame-Options — старіший, але досі корисний заголовок для керування вбудовуванням.
Заборонити вбудовування всюди:
X-Frame-Options: DENYДозволити вбудовування лише з того самого джерела:
X-Frame-Options: SAMEORIGINНаприклад:
X-Frame-Options: SAMEORIGINозначає, що сторінку можна вбудувати лише зі сторінки того самого origin.
X-Frame-OptionsЗначення ALLOW-FROM застаріло і не підтримується сучасними браузерами як надійний спосіб дозволити конкретний список джерел:
X-Frame-Options: ALLOW-FROM https://partner.exampleЯкщо потрібно дозволити кілька конкретних джерел, використовуйте frame-ancestors:
Content-Security-Policy: frame-ancestors https://partner.example https://admin.example.comНа практиці часто надсилають обидва заголовки:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYCSP є сучасним і гнучкішим механізмом, а X-Frame-Options додає сумісність зі старішими браузерами та інструментами.
Нижче наведено мінімальний HTTP-сервер без сторонніх бібліотек. Він віддає сторінку із захистом від вбудовування.
const http = require("node:http");
const server = http.createServer((request, response) => {
response.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
// Забороняємо вбудовувати сторінку в будь-який iframe.
"Content-Security-Policy": "frame-ancestors 'none'",
// Резервний захист для старіших клієнтів.
"X-Frame-Options": "DENY"
});
response.end(`
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>Захищена сторінка</title>
</head>
<body>
<h1>Захищена сторінка</h1>
<p>Цю сторінку не можна вбудувати в iframe.</p>
</body>
</html>
`);
});
server.listen(3000, () => {
console.log("Сервер запущено на http://localhost:3000");
});Запустіть файл:
node server.jsПісля цього спроба вбудувати http://localhost:3000 в iframe з іншої сторінки буде заблокована браузером.
Іноді вбудовування є частиною функціональності:
сторінка оплати;
віджет підтримки;
відеоплеєр;
внутрішня панель адміністратора;
інтеграція між кількома сайтами.
У такому разі не потрібно безумовно забороняти iframe. Натомість задайте мінімально необхідний список дозволених джерел:
Content-Security-Policy: frame-ancestors https://app.example.com
X-Frame-Options: SAMEORIGINОднак тут є важливий нюанс: X-Frame-Options: SAMEORIGIN не дозволяє довільний зовнішній сайт, навіть якщо CSP дозволяє https://app.example.com. Тому для нових браузерів політику визначатиме CSP, але для узгодженої поведінки слід перевірити підтримувані браузери та вимоги проєкту.
Якщо сторінка має вбудовуватися з кількох зовнішніх джерел, основним механізмом має бути frame-ancestors.
frame-ancestors і frame-src — різні директивиЦі директиви часто плутають.
frame-ancestorsВизначає, хто може вбудувати поточну сторінку:
Content-Security-Policy: frame-ancestors 'self' https://portal.exampleframe-srcВизначає, які сторінки поточна сторінка може вбудувати у свої iframe:
Content-Security-Policy: frame-src 'self' https://video.exampleНаприклад:
Content-Security-Policy:
frame-ancestors 'none';
frame-src 'self' https://video.exampleТут поточну сторінку не можна вбудувати в інші сторінки, але вона сама може вбудувати відео з https://video.example.
<meta> для frame-ancestorsДиректива frame-ancestors має надсилатися як HTTP-заголовок:
Content-Security-Policy: frame-ancestors 'none'Не покладайтеся на такий варіант:
<meta
http-equiv="Content-Security-Policy"
content="frame-ancestors 'none'"
>frame-ancestors не підтримується через CSP у <meta> як заміна HTTP-заголовку. Політику потрібно встановлювати на сервері або на рівні reverse proxy.
Для перевірки можна створити сторінку-тест:
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>Перевірка iframe</title>
</head>
<body>
<h1>Сторінка, яка намагається вбудувати ресурс</h1>
<iframe
src="http://localhost:3000"
width="600"
height="300"
title="Тестове вбудовування">
</iframe>
</body>
</html>Відкрийте цю сторінку в браузері та перегляньте DevTools → Console. Якщо захист працює, браузер повідомить, що завантаження було заблоковано політикою frame-ancestors або X-Frame-Options.
Також заголовки можна перевірити через командний рядок:
curl -I http://localhost:3000У відповіді мають бути присутні:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYlocation /account/ {
add_header Content-Security-Policy "frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
}Параметр always важливий, щоб заголовок додавався не лише до успішних відповідей, а й до відповідей з помилками.
Header always set Content-Security-Policy "frame-ancestors 'none'"
Header always set X-Frame-Options "DENY"Для такого налаштування сервер має мати увімкнений модуль, який підтримує директиву Header.
Захист від вбудовування зменшує ризик clickjacking, але не вирішує всі проблеми безпеки.
Для важливих операцій також варто:
вимагати повторного введення пароля;
використовувати підтвердження через MFA;
показувати зрозумілий опис операції;
застосовувати CSRF-захист;
не виконувати небезпечні дії лише за одним кліком;
перевіряти стан сесії та права користувача на сервері.
Заголовки безпеки не повинні бути єдиним захистом критичної операції.
Перевірка window.top !== window.self може бути додатковою діагностикою, але не замінює HTTP-заголовки.
frame-src замість frame-ancestorsframe-src контролює вбудовування інших сторінок поточним документом. Для захисту поточного документа потрібна frame-ancestors.
<meta>Для frame-ancestors необхідний HTTP-заголовок.
Захищати потрібно всі сторінки, які можуть виконувати важливі дії, зокрема:
/account/settings;
/billing;
/admin;
/delete-account.
Таке правило фактично дозволяє вбудовування будь-де:
Content-Security-Policy: frame-ancestors *Для приватних сторінок безпечніше використовувати:
Content-Security-Policy: frame-ancestors 'none''self'Ключове слово 'self' має бути в одинарних лапках:
Content-Security-Policy: frame-ancestors 'self'Без лапок це буде некоректним або неочікуваним значенням.
Origin із JavaScriptЗначення window.location.ancestorOrigins або перевірки через клієнтський JavaScript не можуть замінити політику сервера. Рішення про дозвіл вбудовування має приймати браузер на основі заголовків відповіді.
Для приватної або адміністративної сторінки:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYДля сторінки, яку дозволено вбудовувати лише у власному сайті:
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGINДля інтеграції з конкретним партнером:
Content-Security-Policy: frame-ancestors https://partner.exampleПісля налаштування перевірте:
чи є заголовки у всіх потрібних відповідях;
чи блокується вбудовування з несанкціонованого джерела;
чи працює дозволене вбудовування;
чи не використовуються застарілі або надто широкі правила.
Clickjacking використовує вбудовану у iframe справжню сторінку для приховування небезпечних кліків.
Основний сучасний захист — HTTP-заголовок CSP із директивою frame-ancestors.
frame-ancestors 'none' повністю забороняє вбудовування.
frame-ancestors 'self' дозволяє вбудовування лише з того самого origin.
Для конкретних партнерів можна вказати список дозволених джерел.
X-Frame-Options: DENY і SAMEORIGIN корисні як резервний механізм сумісності.
frame-ancestors і frame-src виконують різні завдання.
JavaScript frame busting не є надійною заміною серверним заголовкам.
CSP для frame-ancestors потрібно надсилати через HTTP-заголовок, а не через <meta>.