JavaScriptMiddleTechnical

Как защититься от 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

Related topics

Как защититься от XSS в браузерном приложении? Где помогают CSP, escaping и Trusted Types? | Talanto