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

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

Джек Джей
Jack J

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

Чтобы снизить этот риск, новое предложение Sanitizer API направлено на создание надежного процессора для безопасной вставки произвольных строк на страницу. В этой статье представлен 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, исключить теги и атрибуты, которые считаются вредными, и сохранить безвредные.

Предлагаемая спецификация Sanitizer API призвана обеспечить такую ​​обработку в качестве стандартного 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 — известная библиотека, предлагающая функции очистки. Основное различие между Sanitizer API и 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. Запуск Намерение отправить на M105

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

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

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

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

Хром

Chrome находится в процессе внедрения Sanitizer API. В 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 и у вас есть отзывы, мы будем рады их услышать. Поделитесь своими мыслями о проблемах Sanitizer API GitHub и обсудите их с авторами спецификаций и людьми, интересующимися этим API.

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

Демо

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

Ссылки


Фото Towfiqu barbhuiya на Unsplash .