Безопасное манипулирование 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 Полный
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 и поделитесь подробностями, чтобы помочь разработчикам отслеживать проблему.

Демо

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

Ссылки


Фото Towfiqu barbhuiya на Unsplash .