Изменить содержимое элемента: 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.Как вставить разметку, если данные всё-таки от пользователя?
createElement + textContent — тогда вставлять нечего.