XSS (Cross-Site Scripting) — класс уязвимостей, позволяющий внедрять и исполнять произвольный JavaScript-код в браузере пользователя. В случае работы с DOM речь чаще всего идёт о DOM-based XSS, при которой вредоносный код не приходит с сервера, а формируется и исполняется непосредственно в клиентском JavaScript.
В отличие от отражённых (reflected) и хранимых (stored) XSS, здесь источник данных — это:
window.location (URL, hash, query-параметры)document.cookielocalStorage / sessionStoragepostMessageОсновная проблема — небезопасная вставка этих данных в DOM без фильтрации или экранирования.
Данные, потенциально контролируемые злоумышленником:
location.searchlocation.hashdocument.referrerwindow.namelocalStorage.getItem()Места, где данные могут привести к выполнению кода:
innerHTMLouterHTMLinsertAdjacentHTMLdocument.writeevalsetTimeout / setInterval (со строкой)new Function()Пример уязвимого кода:
const hash = window.location.hash.substring(1);
document.getElementById('output').innerHTML = hash;
Если URL содержит:
#<img src=x oner ror=alert(1)>
произойдёт выполнение alert(1).
Библиотека Headroom.js управляет поведением заголовков (header) при прокрутке страницы. Она не занимается обработкой пользовательских данных напрямую, но часто используется в проектах, где:
Это создаёт условия, при которых XSS может быть внедрён через сопутствующий код.
const header = document.querySelector("header");
const headroom = new Headroom(header);
headroom.init();
Проблема возникает не в самой библиотеке, а в окружении:
const title = new URLSearchParams(location.search).get('title');
header.innerHTML = `<h1>${title}</h1>`;
Если title содержит вредоносный код, он будет вставлен в
DOM и выполнен.
element.innerHTML = userInput;
Опасность: интерпретация HTML и выполнение встроенных обработчиков
(onerror, onclick).
element.innerHTML = `<div>${data}</div>`;
Даже при использовании современных возможностей JavaScript проблема остаётся.
element.setAttribute("onclick", userInput);
Позволяет внедрить произвольный JavaScript.
eval(userInput);
Прямое выполнение кода — критическая уязвимость.
element.textContent = userInput;
Гарантирует, что данные будут интерпретированы как текст.
const div = document.createElement("div");
div.textContent = userInput;
element.appendChild(div);
Исключает интерпретацию HTML.
if (isSafeUrl(url)) {
element.setAttribute("href", url);
}
Разные контексты требуют разных подходов:
| Контекст | Метод защиты |
|---|---|
| HTML | HTML-экранирование |
| Атрибут | Экранирование + whitelist |
| JavaScript | JSON.stringify |
| URL | encodeURIComponent |
Пример:
const safe = encodeURIComponent(userInput);
link.href = `/search?q=${safe}`;
CSP — механизм браузера, ограничивающий выполнение скриптов.
Пример заголовка:
Content-Security-Policy: default-src 'self'; script-src 'self'
Преимущества:
Когда необходимо вставлять HTML (например, пользовательский контент), применяется очистка:
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;
Удаляет:
<script>onerror, onclick)jav * ascript:)element.oncl ick = new Function(userInput);
или:
setTimeout(userInput, 1000);
Такие конструкции позволяют выполнить любой код.
Безопасный вариант:
setTimeout(() => {
console.log("safe");
}, 1000);
Headroom.js часто используется на страницах с прокруткой и якорями
(#section).
Уязвимость:
const section = location.hash.slice(1);
document.getElementById(section).scrollIntoView();
Если section используется далее в HTML:
element.innerHTML = section;
возникает риск XSS.
const data = localStorage.getItem("content");
element.innerHTML = data;
Если злоумышленник смог записать данные в localStorage,
они будут выполнены.
Решение:
Основные подходы:
Пример:
function isSafeText(str) {
return /^[a-zA-Z0-9\s]+$/.test(str);
}
Автоматический поиск:
innerHTMLevalИзоляция логики
Отказ от динамической HTML-вставки
Контроль данных из URL
Минимизация inline-скриптов
Использование CSP
const params = new URLSearchParams(location.search);
const title = params.get("title");
const header = document.querySelector("header");
const h1 = document.createElement("h1");
h1.textContent = title || "Default";
header.appendChild(h1);
const headroom = new Headroom(header);
headroom.init();
innerHTML «для удобства»Последствия:
Особенно критично при:
Современные фреймворки (React, Vue) минимизируют риск XSS за счёт:
Однако при использовании:
dangerouslySetInnerHTMLриски возвращаются.