Как защититься от XSS в браузерном приложении? Где помогают CSP, escaping и Trusted Types?
XSS предотвращается эскейпингом вывода, строгой CSP (запрет inline-скриптов, указание allowlist доменов) и Trusted Types, которые принудительно требуют обработки данных перед записью в опасные sink'и DOM.
XSS: три рубежа защиты
Cross-Site Scripting возникает, когда недоверенные данные попадают в DOM без обработки. Защита строится на трёх уровнях: экранирование (escaping), политика безопасности контента (CSP) и Trusted Types.
Экранирование вывода
Никогда не вставляйте пользовательские данные через innerHTML, document.write или outerHTML без экранирования. Безопасная альтернатива — textContent и атрибуты через setAttribute:
// Опасно
div.innerHTML = userInput;
// Безопасно
div.textContent = userInput;
// Экранирование вручную (если нужен HTML)
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
div.innerHTML = escapeHtml(userInput);
Для более сложных случаев используйте библиотеку DOMPurify:
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ["b", "i", "em", "strong"],
ALLOWED_ATTR: []
});
div.innerHTML = clean;
Content Security Policy (CSP)
CSP — HTTP-заголовок, который говорит браузеру, откуда можно загружать ресурсы и выполнять скрипты. Строгая политика запрещает inline-скрипты и eval:
// Заголовок на сервере (Node.js / Express)
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
"script-src 'self' 'nonce-RANDOM_NONCE_HERE'",
"object-src 'none'",
"base-uri 'self'",
"require-trusted-types-for 'script'"
].join("; ")
);
С nonce-подходом каждый легитимный <script> получает случайный одноразовый токен:
// Генерация nonce на сервере
import crypto from "crypto";
const nonce = crypto.randomBytes(16).toString("base64");
// В HTML-шаблоне
// <script nonce="${nonce}">...</script>
CSP блокирует выполнение инжектированных скриптов, даже если они попали в DOM, — это второй рубеж, работающий независимо от экранирования.
Trusted Types
Trusted Types (W3C spec, поддерживается в Chrome/Edge) принудительно требует, чтобы все данные, попадающие в опасные DOM-sink'и (innerHTML, eval, setTimeout со строкой), прошли через явно зарегистрированную политику:
// Включить в CSP: require-trusted-types-for 'script'
// Зарегистрировать политику
const policy = trustedTypes.createPolicy("default", {
createHTML(input) {
// Здесь — санитизация, например DOMPurify
return DOMPurify.sanitize(input);
},
createScriptURL(input) {
const url = new URL(input, location.origin);
if (url.origin !== location.origin) {
throw new Error("Недоверенный URL скрипта");
}
return input;
}
});
// Теперь прямой innerHTML выбросит TypeError
// div.innerHTML = "<img onerror=alert(1)>"; // TypeError!
// А через политику — безопасно
div.innerHTML = policy.createHTML("<img onerror=alert(1)>");
// DOMPurify удалит onerror → <img>
Подводные камни
- CSP с
'unsafe-inline'или'unsafe-eval'фактически не защищает — эти директивы нейтрализуют большую часть политики. - nonce должен быть криптографически случайным и уникальным для каждого запроса; статический nonce бесполезен.
DOMPurifyнужно обновлять — в старых версиях были обходы через новые HTML-элементы (например,<math>,<svg>).- Trusted Types ломает сторонние библиотеки, которые используют
innerHTMLнапрямую — потребуется обёртка или форк. href="javascript:..."иsrc="data:text/html,..."— отдельные векторы, не закрытые экранированием строк.- React/Vue экранируют JSX-интерполяции автоматически, но
dangerouslySetInnerHTML/v-htmlоткрывают дыры — всегда санитизируйте перед ними. - DOM-based XSS происходит на клиенте и не виден серверной фильтрации — нужен анализ клиентского кода и тестирование через
location.hash,document.referrer. - CSP report-only режим (
Content-Security-Policy-Report-Only) позволяет развернуть политику без поломок, собирая нарушения черезreport-uriилиreport-to.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.
Sources
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any