Новые альтернативы innerHTML

Источник: «New alternatives to innerHTML»
getHTML, setHTML, setHTMLUnsafe, декларативный shadow DOM и очистка.

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

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

Дополнительные материалы

Предыдущая Статья

Создание ванильного JavaScript signal() с Proxy

Следующая Статья

Парсинг декларативного shadow DOM