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