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

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

Джек Дж.
Jack J

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

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

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

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

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

const user_input = `<em>hello world</em><img src="" onerro>r=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="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`

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

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

API дезинфицирующего средства

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

const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div

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

$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>&quot;/div

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

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

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g 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" <]})> })
//< >divhe<ll><o bw>orld/b/div

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div

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

  • allowAttributes
  • dropAttributes

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

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

$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div

$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<

$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div

$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/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 >})
//< divcustom-e><lemh>ello/custom-elem/div

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

Сравнение с DomPurify

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

const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o 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 Полный
5. Запуск Намерение отправить на M105

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

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

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

Browser Support

  • Chrome: не поддерживается.
  • Edge: не поддерживается.
  • Предварительный просмотр технологии Firefox: поддерживается.
  • Safari: не поддерживается.

Включение через about://flags или опцию CLI

Хром

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

Firefox

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

Обнаружение особенностей

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

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

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

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

Демо

Чтобы увидеть Sanitizer API в действии, посетите Sanitizer API Playground от Майка Уэста :

Ссылки


Фото Towfiqu barbhuiya на Unsplash .