Пошук уроків, статей та іншого контенту
Знайдете DOM-based XSS у небезпечних DOM API та заміните їх безпечними способами роботи з текстом і HTML.
DOM-based XSS — це вразливість, за якої JavaScript-код у браузері вставляє недовірені дані в DOM небезпечним способом. Сервер може взагалі не брати участі в обробці атаки: шкідливий рядок надходить із URL, localStorage, postMessage або іншого джерела й обробляється безпосередньо в браузері.
Типовий ланцюжок має такий вигляд:
Програма отримує дані з недовіреного джерела.
Дані передаються в небезпечний DOM API.
Браузер інтерпретує рядок як HTML або JavaScript.
Вставлений код виконується в контексті сторінки.
Наприклад, сторінка читає параметр name з URL:
https://example.com/?name=ОленаЯкщо значення вставляється через innerHTML, зловмисник може сформувати URL з HTML- або JavaScript-вмістом.
Недовіреними слід вважати не лише дані з форми. Користувач може контролювати або змінити багато інших значень:
location.search — query-параметри URL;
location.hash — фрагмент URL;
location.pathname;
document.referrer;
дані з localStorage і sessionStorage;
повідомлення з window.postMessage;
відповіді API, якщо вони містять введення користувачів;
значення з DOM, якщо раніше вони були вставлені небезпечно;
дані з розширень браузера або сторонніх скриптів.
Сам факт, що дані отримано з URL або сховища браузера, не робить їх безпечними.
innerHTMLinnerHTML інтерпретує переданий рядок як HTML:
const message = new URLSearchParams(location.search).get("message");
document.querySelector("#output").innerHTML = message;Якщо URL містить HTML, браузер спробує створити відповідні DOM-вузли.
Небезпечний приклад повної сторінки:
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>Небезпечний приклад</title>
</head>
<body>
<h1>Повідомлення</h1>
<div id="output"></div>
<script>
const message = new URLSearchParams(location.search).get("message") ?? "";
// Небезпечно: рядок обробляється як HTML
document.querySelector("#output").innerHTML = message;
</script>
</body>
</html>Для демонстрації проблеми можна передати в параметрі HTML, наприклад:
?message=<img src=x onerror="alert('XSS')">У реальному застосунку замість alert зловмисник може спробувати викрасти дані сторінки, виконати дії від імені користувача або змінити її вміст.
insertAdjacentHTMLЦей метод також вставляє рядок як HTML:
const comment = getCommentFromUser();
// Небезпечно
document.querySelector("#comments")
.insertAdjacentHTML("beforeend", `<p>${comment}</p>`);Навіть якщо зовнішня розмітка здається безпечною, значення comment може містити HTML або обробники подій.
outerHTMLПрисвоювання outerHTML замінює весь елемент і також інтерпретує рядок як HTML:
const title = getTitleFromUser();
// Небезпечно
document.querySelector("#title").outerHTML = `<h2>${title}</h2>`;document.writedocument.write вставляє HTML у документ і майже завжди є поганим вибором для сучасного коду:
const value = getValueFromUser();
// Небезпечно
document.write(value);Окрім XSS, цей API може перезаписати документ, якщо викликати його після завершення завантаження сторінки.
DOM-based XSS не обмежується HTML. Небезпечними є API, які виконують рядок як код:
const code = location.hash.slice(1);
// Небезпечно
eval(code);До таких механізмів належать:
eval;
new Function(...);
setTimeout("рядок");
setInterval("рядок").
Слід передавати їм функції, а не рядки:
setTimeout(() => {
console.log("Безпечний виклик");
}, 1000);Якщо значення має бути саме текстом, використовуйте textContent:
const message = new URLSearchParams(location.search).get("message") ?? "";
document.querySelector("#output").textContent = message;textContent створює текстовий вузол. Символи < і > не стають HTML-тегами.
Повний безпечний приклад:
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>Безпечне повідомлення</title>
</head>
<body>
<h1>Повідомлення</h1>
<div id="output"></div>
<script>
const params = new URLSearchParams(location.search);
const message = params.get("message") ?? "";
// Безпечно: значення відображається як звичайний текст
document.querySelector("#output").textContent = message;
</script>
</body>
</html>Якщо відкрити сторінку з таким параметром:
?message=<img src=x onerror="alert('XSS')">на сторінці буде показано сам текст, а не створено зображення з обробником події.
Для складнішої структури створюйте елементи через document.createElement, а текст встановлюйте через textContent.
const comments = [
{
author: "Олена",
text: "Корисний матеріал"
},
{
author: "<script>alert('XSS')</script>",
text: "Текст коментаря"
}
];
const list = document.querySelector("#comments");
for (const comment of comments) {
const item = document.createElement("article");
const author = document.createElement("h2");
const text = document.createElement("p");
author.textContent = comment.author;
text.textContent = comment.text;
item.append(author, text);
list.append(item);
}HTML-контейнер:
<section id="comments"></section>У цьому прикладі значення author і text не можуть перетворити частину коментаря на HTML.
innerHTML не завжди забороненийinnerHTML сам по собі не є вразливістю. Вразливість виникає, коли в нього потрапляють недовірені дані.
Безпечний приклад із повністю статичним HTML:
document.querySelector("#status").innerHTML = `
<strong>Готово</strong>
<span>Дані збережено.</span>
`;Тут рядок не містить даних користувача або зовнішнього джерела.
Однак навіть у статичній розмітці варто не змішувати HTML і недовірені значення:
const username = getUsernameFromUser();
// Небезпечно
element.innerHTML = `<p>Вітаємо, ${username}!</p>`;Безпечніший варіант:
const paragraph = document.createElement("p");
paragraph.textContent = `Вітаємо, ${username}!`;
element.replaceChildren(paragraph);Метод replaceChildren зручно використовувати, коли потрібно очистити контейнер і вставити нові вузли.
Ризик залежить від того, який саме атрибут встановлюється.
Для звичайних атрибутів можна використовувати setAttribute, якщо назва атрибута статична, а значення проходить відповідну перевірку:
const image = document.createElement("img");
image.setAttribute("alt", userProvidedDescription);Для властивостей елементів часто зручніше використовувати DOM-властивості:
const input = document.createElement("input");
input.type = "text";
input.value = userProvidedValue;Особливо обережно працюйте з такими значеннями:
onclick, onerror та іншими inline-обробниками;
href і src, якщо вони можуть містити URL від користувача;
URL зі схемами javascript:, data: та подібними;
style, якщо значення будується з введення користувача.
Замість inline-обробника події використовуйте addEventListener:
const button = document.querySelector("#save");
button.addEventListener("click", () => {
console.log("Дані збережено");
});Якщо користувач може вказати посилання, не вставляйте його без перевірки:
const link = document.createElement("a");
const rawUrl = userProvidedUrl;
try {
const url = new URL(rawUrl, location.origin);
if (url.protocol === "https:" || url.protocol === "http:") {
link.href = url.href;
link.textContent = "Перейти";
} else {
link.textContent = "Непідтримуваний тип посилання";
}
} catch {
link.textContent = "Некоректне посилання";
}Важливо перевіряти не лише формат URL, а й дозволену схему. Просте порівняння рядка з префіксом або пошук підрядка може бути ненадійним.
Іноді застосунок справді має дозволяти форматування: жирний текст, списки або посилання. У такому разі textContent не підходить, бо він покаже HTML як текст.
Потрібен процес санітизації:
Визначити дозволені теги.
Визначити дозволені атрибути.
Видалити скрипти, inline-обробники подій і небезпечні URL.
Вставити результат у DOM.
Не слід намагатися написати повноцінну санітизацію регулярними виразами. HTML має складний синтаксис, тому наївне видалення рядків на кшталт <script> не гарантує безпеки.
У проєкті слід використовувати перевірений санітайзер, налаштований за принципом allowlist. Якщо бібліотеку додано до проєкту, її конфігурацію потрібно обмежити лише потрібними тегами й атрибутами.
Також важливо пам’ятати: DOMParser не є санітайзером. Він лише перетворює рядок на DOM і не робить його безпечним автоматично.
Під час аудиту зручно розділяти код на дві частини.
Джерело — місце, звідки надходять дані:
const valueFromUrl = new URLSearchParams(location.search).get("value");
const valueFromStorage = localStorage.getItem("value");
const valueFromHash = location.hash;Приймач — API, яке може небезпечно обробити ці дані:
element.innerHTML = value;
element.outerHTML = value;
element.insertAdjacentHTML("beforeend", value);
eval(value);Під час перевірки шукайте шлях від джерела до приймача:
const query = new URLSearchParams(location.search);
const username = query.get("username") ?? "";
const greeting = document.querySelector("#greeting");
// Приймач: недовірене значення потрапляє в HTML
greeting.innerHTML = `<p>Привіт, ${username}!</p>`;Виправлення:
const query = new URLSearchParams(location.search);
const username = query.get("username") ?? "";
const greeting = document.querySelector("#greeting");
const paragraph = document.createElement("p");
paragraph.textContent = `Привіт, ${username}!`;
greeting.replaceChildren(paragraph);postMessageПовідомлення з postMessage також потрібно перевіряти. Не можна безумовно вставляти отриманий рядок у innerHTML.
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted.example") {
return;
}
if (!event.data || typeof event.data !== "object") {
return;
}
const output = document.querySelector("#output");
output.textContent = String(event.data.message ?? "");
});Перевірка origin повинна відповідати конкретному дозволеному джерелу. Не використовуйте "*" як заміну перевірці, якщо це не виправдано архітектурою повідомлень.
Для кожного місця, де дані виводяться в DOM, перевірте значення з такими символами:
< > " ' & /Також протестуйте рядки, що нагадують HTML:
<img src=x onerror="alert('test')">
<script>alert('test')</script>
</script><script>alert('test')</script>Після безпечного виправлення потрібно переконатися, що:
рядок відображається як текст;
нові HTML-елементи не створюються;
обробники подій не виконуються;
значення не запускається через eval або рядковий setTimeout;
перевірка URL не дозволяє небезпечні схеми;
тестування виконується в ізольованому середовищі.
Не використовуйте для перевірки реальні токени, cookie або приватні дані.
Функції, які замінюють кілька символів через replace, легко зробити неповними або неправильними для різних контекстів.
Для виведення звичайного тексту використовуйте textContent, а не власну функцію екранування.
innerHTML на insertAdjacentHTMLЦе не виправлення. Обидва API інтерпретують переданий рядок як HTML.
innerText замість textContent без розуміння різниціinnerText також не інтерпретує значення як HTML, але залежить від стилів і відображення елемента та може спричиняти перерахунок layout. Для безпечного встановлення тексту зазвичай достатньо textContent.
localStorage надійнимБудь-який JavaScript-код у межах того самого origin може змінити localStorage. Не вставляйте його значення в HTML без обробки.
scriptXSS може використовувати не лише тег <script>. Небезпечними можуть бути обробники подій, URL зі спеціальними схемами, SVG та інші контексти.
encodeURIComponent універсальним захистомencodeURIComponent призначений для кодування компонентів URL, а не для безпечної вставки в HTML, JavaScript або CSS. Захист має відповідати контексту використання.
DOMParser санітизацієюПарсинг HTML не видаляє небезпечний вміст. Після DOMParser все одно потрібна санітизація або безпечний спосіб побудови DOM.
Серверна валідація важлива, але DOM-based XSS може виникнути повністю на клієнті. Кожне місце вставки даних у DOM потрібно захищати в браузері.
Знайдіть джерело недовірених даних.
Простежте, у який DOM API вони потрапляють.
Визначте, що має бути результатом: текст чи HTML.
Для тексту використайте textContent, value або створення DOM-вузлів.
Для подій використовуйте addEventListener.
Для URL перевірте схему й формат.
Якщо потрібен HTML, застосуйте спеціалізовану санітизацію з allowlist.
Протестуйте спеціальними символами та XSS-подібними рядками.
Додайте автоматизований тест, щоб уразливість не повернулася.
DOM-based XSS виникає, коли недовірені дані небезпечно обробляються JavaScript-кодом у браузері.
Основні ризики пов’язані з innerHTML, outerHTML, insertAdjacentHTML, document.write та виконанням рядків як JavaScript.
Для відображення тексту використовуйте textContent.
Для складної структури створюйте елементи через createElement, а дані додавайте як текст.
Обробники подій підключайте через addEventListener.
URL потрібно перевіряти за допомогою парсингу та allowlist дозволених схем.
Якщо застосунку потрібен HTML від користувача, використовуйте спеціалізовану санітизацію, а не регулярні вирази.
Під час аудиту шукайте шлях від недовіреного джерела до небезпечного приймача.