Изменить содержимое элемента: html(), text() и XSS

Содержание

О проверке кода. Примеры не запускались в браузере — в отличие от материалов по Node.js, где приводится фактический вывод. Код дан по документации jQuery и MDN. Говорим прямо, а не ставим бейдж «проверено» без проверки.

Заменить содержимое элемента — задача на одну строку, и выбор метода выглядит делом вкуса. Он им не является.

Четыре способа

// jQuery
$('#box').html('<b>жирный</b>');   // разбирает как разметку
$('#box').text('<b>жирный</b>');   // покажет буквально: <b>жирный</b>

// нативно
box.innerHTML = '<b>жирный</b>';   // разбирает как разметку
box.textContent = '<b>жирный</b>'; // покажет буквально
Задача jQuery Нативно
Вставить разметку .html(str) innerHTML = str
Вставить текст .text(str) textContent = str
Прочитать разметку .html() innerHTML
Прочитать текст .text() textContent
Очистить .empty() textContent = ''
Заменить сам элемент .replaceWith(str) el.replaceWith(node)

Синтаксически jQuery ничего не выигрывает — разница в пару символов.

Главное: это выбор про безопасность

.html() и innerHTML разбирают строку как разметку. Если в строке есть теги — они станут элементами. Если строка пришла от пользователя — вы вставили в страницу чужой код.

// ❌ XSS: имя пришло из формы или из URL
$('#greeting').html('Привет, ' + userName);
greeting.innerHTML = 'Привет, ' + userName;

// ✅ безопасно всегда
$('#greeting').text('Привет, ' + userName);
greeting.textContent = 'Привет, ' + userName;

textContent не разбирает разметку вообще. Что бы там ни было — теги, кавычки, скрипты — всё станет видимым текстом. Атаковать нечем.

Куда попадает строка пользователя

innerHTML / .html()

Строка разбирается как разметка

  • Браузер исполняет то, что распарсил
  • <img src=x onerror=…> сработает
  • Фильтровать «плохое» бесполезно — способов обхода десятки
  • Законно: только своя разметка или после санитайзера

textContent / .text()

Строка остаётся строкой

  • Разметка не разбирается вовсе
  • Теги видны как обычные символы
  • Атаковать нечего по устройству
  • Выбор по умолчанию для любых данных

Та же логика, что у execFile вместо exec и параметризованных запросов SQL: безопасность даёт не фильтрация, а отсутствие интерпретатора. Данные не попадают туда, где их могут выполнить.

Миф про script

Самое живучее заблуждение на эту тему:

«innerHTML не выполняет <script>, значит это безопасно.»

Первая часть верна — по спецификации скрипт, вставленный через innerHTML, не запустится. Вторая часть ложная, и вот почему:

// script действительно не выполнится
box.innerHTML = '<script>alert(1)</script>';

// а это выполнится прекрасно
box.innerHTML = '<img src=x onerror="alert(document.cookie)">';
box.innerHTML = '<svg onload="alert(1)">';
box.innerHTML = '<iframe src="javascript:alert(1)">';

Обработчики событий в атрибутах работают всегда. Картинка с несуществующим src гарантированно вызовет onerror — это не хак, а штатное поведение. Запрет на <script> закрывает один вектор из десятков.

Отсюда вывод: «я убрал теги script» — не защита. Самописные фильтры обходят всегда; это гонка, в которой атакующему нужно найти одну дырку, а вам — закрыть все.

Если разметка всё-таки нужна

Бывает: комментарий с форматированием, письмо из редактора. Два честных пути.

Санитайзер:

import DOMPurify from 'dompurify';
box.innerHTML = DOMPurify.sanitize(userHtml);

DOMPurify — не «фильтр плохого», а разбор разметки с последующей сборкой только из разрешённого. Белый список, как и должно быть.

Собрать DOM программно:

const p = document.createElement('p');
p.textContent = userText;         // текст остаётся текстом
const b = document.createElement('b');
b.textContent = userName;
p.append(' — ', b);
box.replaceChildren(p);

Многословнее, зато вставлять нечего: каждый кусок текста попадает в textContent, а структуру задаёте вы.

textContent или innerText

Мелочь, но с последствиями для производительности:

box.textContent;   // весь текст, включая скрытый CSS-ом
box.innerText;     // только видимый — и это вызывает reflow

innerText учитывает отображение: он спрашивает у браузера, что реально видно, и потому заставляет пересчитать стили. В цикле по сотне элементов это заметно.

По умолчанию берите textContent. innerText нужен ровно тогда, когда важен видимый текст с учётом переносов и скрытых блоков.

Что было на этой странице в 2017 году

Первая версия вышла 15 августа 2017 года и разбирала .html(), .text(), .append() и .empty(). Синтаксис описан верно, за девять лет не изменился.

Чего в ней не было — ни слова про XSS. .html() и .text() подавались как равноправные способы «вставить содержимое», а выбор между ними — как вопрос того, нужны вам теги или нет.

Это точный снимок фронтенда 2017 года: страницы собирались на сервере, а JavaScript чаще двигал уже готовую разметку, чем вставлял чужие данные. Сегодня всё наоборот — фронтенд работает с пользовательским вводом постоянно, и .html() из удобного метода превратился в опасный.

Синтаксис не устарел. Устарело представление, что выбор между ними — дело вкуса.


Смежные темы: изменение стилей и классов; установка атрибутов — там про ту же проблему в href; уроки JavaScript.

Частые вопросы

Чем `.html()` отличается от `.text()`?
.html() разбирает строку как разметку — теги станут элементами. .text() вставляет строку как текст — теги останутся видимыми символами. Это не стилистика: .html() с данными пользователя — это XSS.
`innerHTML` с данными пользователя — точно опасно? Ведь `<script>` не выполняется.
Опасно. <script> через innerHTML действительно не запустится, но это ничего не решает: строка <img src=x onerror=alert(1)> выполнит код прекрасно. Обработчики событий в атрибутах работают всегда.
Чем `textContent` отличается от `innerText`?
textContent возвращает весь текст, включая скрытый CSS-ом, и не запускает пересчёт стилей. innerText учитывает отображение и потому вызывает reflow — он медленнее. По умолчанию берите textContent.
Как вставить разметку, если данные всё-таки от пользователя?
Санитизировать библиотекой вроде DOMPurify, а не самописным фильтром. Либо собирать DOM программно: createElement + textContent — тогда вставлять нечего.