Как предотвратить XSS-атаки на основе DOM с помощью Trusted Types

Веб-функции: Надежные типы

Trusted Types – это функция безопасности, которая помогает предотвратить межсайтовый скриптинг на основе DOM (DOM XSS). Она требует, чтобы данные, передаваемые в "опасные" API браузера (известные как приемники), сначала преобразовывались в определенный тип, например TrustedHTML.

По умолчанию многие API DOM (например, innerHTML или document.write) принимают необработанные строки. Если строка содержит вредоносный скрипт из ненадежного источника, он может быть выполнен. Trusted Types меняет это поведение: после включения через CSP эти приемники будут принимать только объекты TrustedHTML, созданные авторизованными правилами.

Основные этапы реализации

Чтобы использовать доверенные типы, выполните четыре основных шага:

  1. Определите политику безопасности, которая указывает, как очищать или преобразовывать строки.
  2. Используйте это правило, чтобы создавать объекты доверенных типов из необработанных строк.
  3. Используйте объекты Trusted Types при записи в приемники DOM.
  4. Принудительно используйте доверенные типы с помощью Content Security Policy (CSP).

1. Создание политики

Правила – это набор инструкций по очистке контента. Вы создаете его с помощью trustedTypes.createPolicy(), обычно делегируя библиотеке очистки, такой как DOMPurify, или правилам экранирования HTML вашего приложения:

import DOMPurify from 'dompurify';

const myPolicy = trustedTypes.createPolicy('sanitize-html', {
  createHTML: (input) =>
    // Return a sanitized string; createPolicy() wraps it in a TrustedHTML object.
    // Example using DOMPurify:
    DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
});

// Example using HTML escaping (for environments without third-party sanitizers):
const escapePolicy = trustedTypes.createPolicy('escape-html', {
  createHTML: (input) =>
    String(input)
      .replaceAll('&', '&')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#39;'),
});

2. Использование правил

Вместо того чтобы передавать строку непосредственно в приемник, вы вызываете метод создания правила.

const untrustedInput = '<p>Hello, <strong>world</strong>!</p><img src="x" onerror="alert(1)">';

// In an enforced environment, this would throw a TypeError:
// element.innerHTML = untrustedInput;

// This returns a TrustedHTML object with unsafe elements and attributes removed:
const cleanHTML = myPolicy.createHTML(untrustedInput);
// Result: "<p>Hello, <strong>world</strong>!</p><img src=\"x\">"

3. Переход на использование доверенных типов в приемниках

Обновите каждый экземпляр, в котором вы записываете данные в приемник, чтобы сначала преобразовать его в объект Trusted Type. Чтобы найти места, где нужно обновить код, можно определить временное правило default, которое будет регистрировать случаи, когда в приемник записывается строка, а не TrustedType.

trustedTypes.createPolicy("default", {
  createHTML(value) {
    console.warn("String passed instead of Trusted Type.");
    return DOMPurify.sanitize(value, { RETURN_TRUSTED_TYPE: false });
  },
});

К распространенным приемникам, поддерживающим доверенные типы, относятся:

  • HTML: element.innerHTML, element.outerHTML, document.write(), element.setHTMLUnsafe(), Document.parseHTMLUnsafe().
  • Скрипт: <script> текстовый контент, eval()
  • ScriptURL: <script src>, Worker(), SharedWorker()
// Assign the TrustedHTML object.
element.innerHTML = cleanHTML;

Обратите внимание, что "безопасные" методы очистки HTML (element.setHTML() и Document.parseHTML(), описанные в разделе Очистка ненадежного HTML-кода) не требуют правил Trusted Types, поскольку они всегда очищают потенциальные XSS-атаки нативно.

4. Как принудительно использовать Trusted Types с помощью CSP

Чтобы не нарушить работу приложения, Trusted Types следует внедрять постепенно. Полное применение обеспечивает максимальную защиту, но может привести к сбоям в работе сайта, если не все записи в приемники были перенесены.

Требования

  • Совместимость с фреймворком. Если фреймворк вашего приложения (или любая сторонняя библиотека, которая записывает данные в приемники DOM) не создает значения TrustedHTML или TrustedScript, вы не сможете принудительно использовать доверенные типы, не нарушив работу этого кода. Перед началом развертывания проверьте поддержку фреймворка.
  • Рефакторинг приемников. Убедитесь, что вы выявили опасные приемники и провели их рефакторинг, чтобы использовать правила доверенных типов.

Стратегия развертывания

  1. Только отчеты. Сначала разверните заголовок Content-Security-Policy-Report-Only. В этом случае нарушения будут регистрироваться в конечной точке отчетов, но запись не будет блокироваться.

    Content-Security-Policy-Report-Only: require-trusted-types-for 'script';
    
  2. Принудительное применение. Когда вы интегрируете правила и количество сообщений о нарушениях снизится до нуля, переходите к полному принудительному применению.

    Content-Security-Policy: require-trusted-types-for 'script';
    

Вы также можете ограничить список правил, которые разрешено создавать. Для этого перечислите их названия, например sanitize-html или escape-html. Если ваше правило делегирует полномочия правилу DOMPurify.sanitize(), которое создает правило dompurify, укажите dompurify. Если вы регистрируете правило по умолчанию во время переноса, укажите default.

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types sanitize-html escape-html dompurify;

При использовании этой CSP любая запись в приемник, не использующая доверенные типы, приведет к ошибке. Кроме того, попытка создать правило Trusted Types с названием, не указанным в trusted-types, приведет к ошибке.

Вы также можете задать эту CSP в виде тега <meta> в разделе <head> документа, хотя это может быть менее безопасно.

<!-- First child of `<head>`, as earlier tags will not enforce this CSP. -->
<meta
  http-equiv="Content-Security-Policy"
  content="require-trusted-types-for 'script'; trusted-types sanitize-html escape-html dompurify;"
/>

Резервные стратегии

Для браузеров, которые пока не поддерживают Trusted Types, можно использовать минимальный полифил, чтобы код не ломался и при этом обеспечивалась очистка.

if (!window.trustedTypes) {
  window.trustedTypes = {
    createPolicy: (name, rules) => rules,
  };
}

Это позволяет вам писать код с использованием шаблона доверенных типов уже сегодня. Поддерживающие браузеры будут применять типы, а более старые браузеры по-прежнему будут выполнять вашу логику очистки с помощью фиктивной политики.

Если браузер не поддерживает доверенные типы, он будет обрабатывать объекты TrustedHTML как строки (при использовании tinyfill) или не будет выполнять проверку на уровне приемника.