Новые альтернативы innerHTML
Примечание по браузерной поддержке: setHTMLUnsafe поддерживается во всех браузерах. setHTML всё ещё находится на стадии стандартизации и доступен только в Firefox за флагом. getHTML поддерживается в Chrome и Edge с версии 125.
Недавно в браузерах появился новый метод setHTMLUnsafe. Unsafe в данном контексте означает, что, как и innerHTML, он не выполняет очистку ввода. Такое наименование не соответствует предыдущим API браузера: у нас есть innerHTML, а не innerHTMLUnsafe; eval(), а не evalUnsafe(), и т. д. setHTMLUnsafe, конечно, не более опасен, чем эти старые методы. Однако в отличие от старых методов, существует как безопасная версия (setHTML), так и небезопасная (setHTMLUnsafe) — отсюда и название.
Вот что говорится в спецификации Sanitizer API:
«Безопасные» методы не будут генерировать разметку, выполняющую скрипт. То есть они должны быть защищены от XSS.
Представим, что есть HTML-форма с текстом <input> и некоторый JavaScript-код, изменяющий DOM на основе введённого пользователем значения:
form.addEventListener('submit', function(event) {
event.preventDefault();
const markup = `<h2>${input.value}</h2>`
div.innerHTML = markup;
});Если пользователь введёт в поле ввода <img src=doesnotexist onerror="alert('Potential XSS Attack')">, этот JavaScript-код будет запущен в браузере. У функции .setHTMLUnsafe() та же проблема.
В этом упрощённом примере код выполняется только в браузере самого пользователя, но если подобный пользовательский ввод хранится в базе данных и используется для отображения динамического контента другим пользователям, произвольный и потенциально вредоносный JavaScript может выполняться в браузерах других пользователей.
При использовании setHTML в DOM будет вставлен только <img src="doesnotexist">. Изображение по-прежнему вставляется на страницу, но JavaScript удаляется.
Работа над Sanitizer API ещё не завершена, но она помогает понять контекст именования setHTMLUnsafe.
setHTMLUnsafe
Если мы (надеемся) получим setHTML, и уже есть innerHTML, зачем вообще нужен setHTMLUnsafe? Ответ заключается в декларативном shadow DOM.
HTML элемент <template> может использоваться двумя разными способами:
- Для хранения фрагмента HTML, который не отображается, но может быть использован позже с помощью JavaScript.
- Для немедленной генерации shadow DOM. Если
<template>содержит атрибутshadowrootmode, элемент заменяется в DOM его содержимым, внутри shadow root.
innerHTML отлично справляется с первым вариантом использования, но не может справиться со вторым.
const main = document.querySelector('main');
main.innerHTML = `
<h2>I am in the Light DOM</h2>
<div>
<template shadowrootmode="open">
<style>
h2 { color: blue; }
</style>
<h2>Shadow DOM</h2>
</template>
</div>`innerHTML внедряет <template> в страницу, но он остаётся элементом <template> — он не превращается в shadow DOM и его содержимое не отображается, независимо от атрибута shadowrootmode.
setHTML целенаправленно удалит template и его содержимое:
const main = document.querySelector('main');
main.setHTML(`
<h2>I am in the Light DOM</h2>
<div>
<template shadowrootmode="open">
<style>
h2 { color: blue; }
</style>
<h2>Shadow DOM</h2>
</template>
</div>`);В приведённом выше примере контент main теперь представляет собой h2 и пустой div. template рассматривается как "небезопасный узел".
Именно поэтому браузеры добавили setHTMLUnsafe, как способ динамического добавления декларативного shadow DOM на страницу.
main.setHTMLUnsafe(`
<h2>I am in the Light DOM</h2>
<div>
<template shadowrootmode="open">
<style>
h2 { color: blue; }
</style>
<h2>Shadow DOM</h2>
</template>
</div>
`);При использовании setHTMLUnsafe содержимое <template> будет отображаться внутри shadow DOM.
getHTML
Функции setHTML и setHTMLUnsafe сами по себе не являются полноценной заменой innerHTML. innerHTML может как задавать, так и получать HTML. Дополнительной функцией к setHTML и setHTMLUnsafe является getHTML (небезопасной версии не существует).
const main = document.querySelector('main');
const html = main.getHTML();По умолчанию getHTML не будет возвращать разметку из shadow DOM, но это можно настроить.
const main = document.querySelector('main');
const html = main.getHTML({serializableShadowRoots: true} );Установка значения serializableShadowRoots в true приведёт к сериализации всех shadow DOM деревьев, которые выбрали сериализацию.
Элемент template может отказаться от использования с помощью атрибута shadowrootserializable:
<template shadowrootmode="open" shadowrootserializable>Аналогично, в JavaScript метод attachShadow имеет boolean опцию serializable.
this.attachShadow({ mode: "open", serializable: true });Также можно сериализовать только определённые shadow DOM деревья, передав массив shadow roots:
const markup= main.getHTML({
shadowRoots: [document.querySelector('.example').shadowRoot]
});Все shadow roots в массиве будут сериализированы, даже если они не помечены как сериализируемые.