Пошук уроків, статей та іншого контенту
Дослідите відображуваний і збережений XSS, а також способи запобігання через контекстне кодування.
XSS (Cross-Site Scripting) — це атака, за якої зловмисний JavaScript виконується в браузері іншого користувача в контексті довіреного вебсайту.
Атакувальник зазвичай намагається вставити спеціально сформований текст у вебсторінку. Якщо застосунок сприйме цей текст як HTML або JavaScript замість звичайних даних, код буде виконано.
Наслідки XSS можуть бути різними:
зміна вмісту сторінки;
підміна форми входу;
викрадення даних, доступних JavaScript;
виконання дій від імені користувача;
перенаправлення на інший сайт.
XSS не виникає лише через наявність символів < або >. Проблема з’являється тоді, коли неперевірені дані потрапляють у небезпечний контекст без правильного кодування або очищення.
Відображуваний XSS (reflected XSS) виникає, коли шкідливі дані надходять із запиту, одразу потрапляють у відповідь сервера, а потім виконуються в браузері.
Типовий сценарій:
Сторінка отримує пошуковий запит із URL.
Сервер вставляє цей запит у HTML-відповідь.
Користувач відкриває спеціально сформоване посилання.
Браузер сприймає вставлений фрагмент як HTML або JavaScript.
Наприклад, небезпечний сервер може сформувати відповідь приблизно так:
<h1>Результати пошуку для: <значення_з_URL></h1>Якщо значення з URL вставляється без кодування, воно може закрити тег h1 і додати інший HTML.
У реальному застосунку корисне навантаження може бути передане через параметр URL. Для навчання можна використати такий безпечний демонстраційний рядок:
<img src=x onerror="alert('XSS')">Цей приклад лише демонструє факт виконання коду. У реальних атаках наслідки можуть бути значно серйознішими.
Наведений нижче файл імітує сторінку пошуку. Відкрийте його локально в браузері та введіть у поле:
<img src=x onerror="alert('XSS')"><!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Демонстрація відображуваного XSS</title>
</head>
<body>
<h1>Пошук</h1>
<form id="search-form">
<input id="search-input" autocomplete="off">
<button type="submit">Знайти</button>
</form>
<h2>Небезпечний результат</h2>
<div id="unsafe-result"></div>
<h2>Безпечний результат</h2>
<div id="safe-result"></div>
<script>
const form = document.querySelector('#search-form');
const input = document.querySelector('#search-input');
const unsafeResult = document.querySelector('#unsafe-result');
const safeResult = document.querySelector('#safe-result');
form.addEventListener('submit', (event) => {
event.preventDefault();
const query = input.value;
// Небезпечно: браузер обробляє query як HTML.
unsafeResult.innerHTML = `Результати для: ${query}`;
// Безпечно: query вставляється лише як звичайний текст.
safeResult.textContent = `Результати для: ${query}`;
});
</script>
</body>
</html>У першому блоці значення передається до innerHTML, тому браузер розбирає його як HTML. У другому використовується textContent, який сприймає значення лише як текст.
Не вставляйте подібні рядки на чужих сайтах і не перевіряйте їх без дозволу. Для навчання використовуйте локальний приклад або спеціальне тестове середовище.
Збережений XSS (stored XSS) виникає, коли шкідливі дані зберігаються на сервері, а потім показуються іншим користувачам.
Типовий сценарій:
Користувач додає коментар із небезпечним HTML.
Сервер зберігає коментар у базі даних.
Інший користувач відкриває сторінку з коментарями.
Браузер виконує вставлений код.
Збережений XSS часто небезпечніший за відображуваний, тому що для атаки не потрібно переконувати кожного користувача відкрити спеціальне посилання. Достатньо один раз зберегти шкідливий вміст.
Наступний приклад використовує localStorage замість сервера та бази даних. Це лише модель збереженого XSS.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Демонстрація збереженого XSS</title>
</head>
<body>
<h1>Коментарі</h1>
<form id="comment-form">
<label>
Коментар:
<input id="comment-input" autocomplete="off">
</label>
<button type="submit">Додати</button>
</form>
<button id="clear-button" type="button">Очистити коментарі</button>
<h2>Коментарі</h2>
<ul id="comments"></ul>
<script>
const form = document.querySelector('#comment-form');
const input = document.querySelector('#comment-input');
const commentsList = document.querySelector('#comments');
const clearButton = document.querySelector('#clear-button');
function readComments() {
return JSON.parse(localStorage.getItem('comments') || '[]');
}
function renderComments() {
const comments = readComments();
// Очищаємо список перед повторним відображенням.
commentsList.replaceChildren();
for (const comment of comments) {
const item = document.createElement('li');
// Безпечно: текст не інтерпретується як HTML.
item.textContent = comment;
commentsList.append(item);
}
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const comment = input.value;
const comments = readComments();
comments.push(comment);
localStorage.setItem('comments', JSON.stringify(comments));
input.value = '';
renderComments();
});
clearButton.addEventListener('click', () => {
localStorage.removeItem('comments');
renderComments();
});
renderComments();
</script>
</body>
</html>У цьому прикладі коментар зберігається, але завдяки textContent під час відображення він залишається текстом.
innerHTML і textContenttextContenttextContent встановлює або читає звичайний текст:
const message = document.querySelector('#message');
message.textContent = userInput;Якщо userInput містить HTML-теги, вони будуть показані як текст, а не оброблені браузером.
innerHTMLinnerHTML працює з HTML-розміткою:
container.innerHTML = '<strong>Важливе повідомлення</strong>';Це може бути доречно, якщо застосунок справді повинен створювати HTML. Але передавати до innerHTML неперевірені дані небезпечно:
// Небезпечно, якщо userInput надходить від користувача.
container.innerHTML = userInput;Навіть шаблонні рядки не захищають від XSS:
// Небезпечно: шаблонний рядок лише підставляє значення.
container.innerHTML = `<p>${userInput}</p>`;Шаблонний рядок змінює спосіб складання рядка, але не кодує його вміст.
Для створення елементів безпечніше використовувати DOM API:
const item = document.createElement('li');
item.textContent = userInput;
document.querySelector('ul').append(item);Такий підхід дає змогу окремо створити елемент і встановити його текстовий вміст.
Також варто обережно працювати з атрибутами:
const link = document.createElement('a');
link.textContent = 'Відкрити профіль';
link.href = `/users/${encodeURIComponent(userId)}`;
document.body.append(link);encodeURIComponent кодує частину URL, наприклад значення ідентифікатора або параметра. Проте воно не є універсальним захистом для всіх контекстів.
Одна з головних ідей захисту від XSS — контекстне кодування. Це означає, що дані потрібно кодувати відповідно до місця, де вони використовуються.
Для тексту в HTML найкраще не створювати HTML-рядок вручну, а використовувати textContent.
element.textContent = value;Якщо потрібно власноруч перетворити текст на HTML-сутності, необхідно коректно кодувати щонайменше спеціальні символи:
& → &
< → <
> → >
" → "
' → '
Але ручне кодування легко реалізувати з помилками, тому для звичайного тексту краще використовувати textContent.
Не слід безпосередньо вставляти неперевірений текст у HTML-атрибут:
// Небезпечно.
element.innerHTML = `<input value="${userInput}">`;Безпечніший варіант:
const input = document.createElement('input');
input.value = userInput;
document.body.append(input);URL потребують окремої перевірки. Наприклад, не варто без перевірки підставляти введення користувача в href:
// Небезпечно: значення може бути не очікуваним URL.
link.href = userInput;Якщо застосунок дозволяє лише посилання на HTTPS-ресурси, можна перевірити схему URL:
function setSafeLink(link, value) {
try {
const url = new URL(value, window.location.origin);
if (url.protocol === 'https:' || url.origin === window.location.origin) {
link.href = url.href;
link.textContent = url.href;
} else {
link.removeAttribute('href');
link.textContent = 'Посилання заблоковано';
}
} catch {
link.removeAttribute('href');
link.textContent = 'Некоректне посилання';
}
}Перевірка залежить від вимог конкретного застосунку. Просте блокування рядків на кшталт javascript: не повинно бути єдиним захистом.
Не слід вставляти дані користувача в JavaScript-код:
// Небезпечно.
const code = `showMessage('${userInput}')`;
eval(code);eval та подібні підходи зазвичай не потрібні для звичайної веброзробки. Передавайте дані як значення, а не створюйте з них програмний код.
Іноді застосунок повинен дозволяти обмежений HTML, наприклад:
форматування тексту в редакторі;
жирний або курсивний текст;
списки;
посилання.
У такому випадку textContent може бути недостатньо, адже він покаже теги як звичайний текст. Потрібне очищення HTML — sanitization.
Очищення повинно:
дозволяти лише потрібні теги;
дозволяти лише безпечні атрибути;
видаляти обробники подій на кшталт onclick;
перевіряти URL у посиланнях;
видаляти небезпечні схеми та конструкції.
Для цього зазвичай використовують спеціалізовану перевірену бібліотеку, а не власний набір регулярних виразів.
Регулярні вирази не є надійним універсальним HTML-санітайзером. HTML має складний синтаксис, тому самостійне очищення часто пропускає небезпечні випадки.
Захист має бути не лише у браузері. Сервер повинен:
кодувати значення перед вставкою в HTML-відповідь;
не довіряти даним, які прийшли з форми або URL;
перевіряти формат, довжину та тип даних;
зберігати дані окремо від HTML-шаблонів;
повторно обробляти дані під час кожного виведення.
Перевірка введення допомагає підтримувати правильний формат, але сама по собі не замінює кодування. Наприклад, перевірка, що коментар має не більше 500 символів, не робить HTML у коментарі безпечним.
Content Security Policy (CSP) — це політика безпеки, яку сервер передає браузеру через HTTP-заголовок. Вона обмежує джерела, з яких можна завантажувати скрипти, і може заборонити небезпечні inline-скрипти.
Приклад заголовка:
Content-Security-Policy: default-src 'self'; script-src 'self'CSP є додатковим рівнем захисту, але не замінює правильне кодування та безпечну роботу з DOM.
Cookie із прапорцем HttpOnly не доступні через document.cookie. Це може зменшити наслідки XSS для сесійних cookie.
Проте XSS усе одно може виконувати дії від імені користувача в межах його сесії. Тому HttpOnly не усуває саму вразливість.
innerHTML для текстуelement.innerHTML = username;Якщо значення є звичайним текстом, використовуйте:
element.textContent = username;encodeURIComponentencodeURIComponent корисний для частин URL, але не є захистом для HTML:
// Це не робить безпечним HTML-контекст.
element.innerHTML = encodeURIComponent(userInput);Для тексту в DOM використовуйте textContent.
Перевірка в JavaScript браузера не достатня. Зловмисник може надіслати HTTP-запит без використання вашої форми та її перевірок.
Дані потрібно безпечно обробляти на сервері під час виведення.
<script>XSS може виникати не лише через тег script. Небезпечними можуть бути:
обробники подій;
небезпечні URL;
SVG-контент;
некоректно оброблені атрибути;
інші HTML-конструкції.
Тому видалення одного підрядка <script> не є надійним захистом.
Регулярний вираз може пропустити небезпечну конструкцію або зламати коректну розмітку. Для дозволеного HTML використовуйте спеціалізований санітайзер із чітким списком дозволених можливостей.
Збережені дані не стають безпечними лише тому, що вони вже записані в базу даних. База даних зберігає значення, але не гарантує, що їх безпечно вставляти в HTML.
Перед виведенням будь-якого зовнішнього значення поставте собі такі запитання:
Звідки походить значення?
URL;
форма;
база даних;
API;
localStorage;
інший DOM-елемент.
У який контекст воно потрапить?
текст;
HTML;
атрибут;
URL;
CSS;
JavaScript-код.
Чи можна використати безпечні DOM-методи?
textContent;
createElement;
setAttribute для контрольованих значень;
append.
Чи справді потрібно дозволяти HTML?
якщо ні, зберігайте та показуйте звичайний текст;
якщо так, використовуйте перевірене очищення з дозволеним списком.
XSS дає змогу виконати небезпечний код у браузері користувача.
Відображуваний XSS використовує дані з поточного запиту та одразу повертає їх у відповіді.
Збережений XSS зберігає небезпечні дані та показує їх користувачам пізніше.
innerHTML не слід використовувати для неперевірених даних.
Для звичайного тексту використовуйте textContent.
Для атрибутів, URL і JavaScript потрібне кодування або перевірка саме для відповідного контексту.
Якщо потрібен HTML від користувача, застосовуйте спеціалізоване очищення.
CSP, HttpOnly та інші заголовки є додатковими рівнями захисту, але не замінюють безпечне виведення даних.