Безопасное манипулирование DOM с помощью Sanitizer API

Новый API Sanitizer призван создать надежный обработчик для безопасной вставки произвольных строк на страницу.

Джек Дж.
Jack J

Приложения постоянно работают с ненадежными строками, но безопасное отображение такого контента в HTML-документе может быть сложной задачей. Без достаточной осторожности легко случайно создать возможности для межсайтового скриптинга (XSS), которые могут быть использованы злоумышленниками.

Для снижения этого риска в новом предложении по API Sanitizer предлагается создать надежный обработчик для безопасной вставки произвольных строк на страницу. В этой статье представлен API и объясняется его использование.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

Экранирование пользовательского ввода

При вставке пользовательского ввода, строк запроса, содержимого cookie и т. д. в DOM необходимо корректно экранировать эти строки. Особое внимание следует уделить манипуляциям с DOM через .innerHTML , где неэкранированные строки являются типичным источником XSS-атак.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

Если экранировать специальные HTML-символы во входной строке выше или развернуть её с помощью .textContent , alert(0) не будет выполнен. Однако, поскольку <em> добавленный пользователем, также разворачивается как строка, этот метод нельзя использовать для сохранения оформления текста в HTML.

Лучше всего здесь не убегать , а провести дезинфекцию .

Очистка пользовательского ввода

Разница между побегом и дезинфекцией.

Экранирование — это замена специальных HTML-символов на HTML-сущности .

Санитизация подразумевает удаление семантически вредных частей (таких как выполнение скриптов) из HTML-строк.

Пример

В предыдущем примере тег <img onerror> вызывает выполнение обработчика ошибок, но если удалить обработчик onerror , его можно будет безопасно развернуть в DOM, оставив <em> нетронутым.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

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

Предлагаемая спецификация API Sanitizer направлена ​​на предоставление такой обработки в качестве стандартного API для браузеров.

API дезинфектора

API Sanitizer используется следующим образом:

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

Однако аргумент { sanitizer: new Sanitizer() } является аргументом по умолчанию. Поэтому он может выглядеть примерно так, как показано ниже.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

Стоит отметить, что setHTML() определен в Element . Будучи методом Element , контекст для парсинга понятен сам собой (в данном случае <div> ), парсинг выполняется один раз внутри класса, и результат напрямую развертывается в DOM.

Чтобы получить результат проверки в виде строки, можно использовать .innerHTML из результатов функции setHTML() .

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

Настройка через конфигурацию

API Sanitizer по умолчанию настроен на удаление строк, которые могут инициировать выполнение скрипта. Однако вы также можете добавить собственные настройки в процесс очистки с помощью объекта конфигурации.

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

Следующие параметры определяют, как результат проверки должен обрабатывать указанный элемент.

allowElements : Названия элементов, которые должен сохранять санитайзер.

blockElements : Названия элементов, которые санитайзер должен удалить, сохраняя при этом их дочерние элементы.

dropElements : Названия элементов, которые должен удалить санитайзер, вместе с их дочерними элементами.

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

Вы также можете управлять тем, будет ли средство проверки подлинности разрешать или запрещать определенные атрибуты, с помощью следующих параметров:

  • allowAttributes
  • dropAttributes

Свойства allowAttributes и dropAttributes ожидают списки соответствия атрибутов — объекты, ключами которых являются имена атрибутов, а значениями — списки целевых элементов или подстановочный знак * .

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

Параметр allowCustomElements позволяет разрешать или запрещать использование пользовательских элементов. Если они разрешены, остальные настройки элементов и атрибутов остаются в силе.

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

Поверхность API

Сравнение с DomPurify

DOMPurify — это известная библиотека, предоставляющая функциональность для очистки кода. Основное отличие между API Sanitizer и DOMPurify заключается в том, что DOMPurify возвращает результат очистки в виде строки, которую необходимо записать в DOM-элемент с помощью метода .innerHTML .

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

DOMPurify может служить в качестве резервного варианта, если API Sanitizer не реализован в браузере.

Реализация DOMPurify имеет несколько недостатков. Если возвращается строка, то входная строка анализируется дважды: DOMPurify и .innerHTML . Этот двойной анализ приводит к нерациональному использованию времени обработки, а также может вызвать интересные уязвимости, возникающие в случаях, когда результат второго анализа отличается от первого.

HTML также требует контекста для разбора. Например, <td> имеет смысл в <table> , но не в <div> . Поскольку DOMPurify.sanitize() принимает в качестве аргумента только строку, контекст для разбора пришлось определять приблизительно.

API Sanitizer улучшает подход DOMPurify и предназначен для устранения необходимости в двойном анализе и уточнения контекста анализа.

Статус API и поддержка браузеров

API Sanitizer находится на стадии стандартизации, и Chrome в настоящее время занимается его внедрением.

Шаг Статус
1. Создайте пояснительное видео. Полный
2. Создайте черновик спецификации. Полный
3. Соберите отзывы и доработайте дизайн. Полный
4. Пробная версия Chrome Origin Полный
5. Запуск Намерение отправить на М105

Mozilla: Считает это предложение достойным прототипирования и активно внедряет его .

WebKit: См. ответ в списке рассылки WebKit .

Как включить API Sanitizer

Browser Support

  • Chrome: не поддерживается.
  • Edge: не поддерживается.
  • Firefox: 147.
  • Safari: не поддерживается.

Включение через about://flags или параметр командной строки.

Хром

Chrome находится в процессе внедрения API Sanitizer. В Chrome 93 и более поздних версиях вы можете протестировать его работу, включив флаг about://flags/#enable-experimental-web-platform-features . В более ранних версиях Chrome Canary и Dev-канала вы можете включить его с помощью --enable-blink-features=SanitizerAPI и попробовать прямо сейчас. Инструкции по запуску Chrome с флагами см. в соответствующем руководстве.

Firefox

Firefox также использует API Sanitizer в качестве экспериментальной функции. Чтобы включить его, установите флаг dom.security.sanitizer.enabled в true в about:config .

Обнаружение признаков

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

Обратная связь

Если вы попробуете этот API и у вас появятся какие-либо отзывы, мы будем рады их услышать. Поделитесь своими мыслями в разделе проблем GitHub, касающихся API Sanitizer, и обсудите это с авторами спецификации и людьми, заинтересованными в этом API.

Если вы обнаружите какие-либо ошибки или неожиданное поведение в реализации Chrome, сообщите об этом, создав заявку на исправление . Выберите компоненты Blink>SecurityFeature>SanitizerAPI и предоставьте подробную информацию, чтобы помочь разработчикам отследить проблему.

Демо

Чтобы увидеть API Sanitizer в действии, посетите площадку Sanitizer API Playground, созданную Майком Вестом :

Ссылки


Фото Towfiqu barbhuiya на Unsplash .