Безопасная работа с innerHTML

Использование innerHTML в JavaScript позволяет динамически изменять содержимое элементов страницы. В контексте Stimulus это часто используется для обновления пользовательского интерфейса на основе действий контроллеров. Однако непосредственное присваивание HTML-кода может быть источником уязвимостей XSS (Cross-Site Scripting), если данные не проходят должную фильтрацию. Stimulus предоставляет инструменты, которые позволяют безопасно управлять DOM без риска внедрения вредоносного кода.


Контроллеры и динамический HTML

Контроллер Stimulus описывается с помощью класса, наследующего Controller. Для работы с HTML часто используется метод this.element.innerHTML:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["output"];

  updateContent() {
    const userInput = "<script>alert('XSS')</script>";
    this.outputTarget.innerHTML = userInput; // Опасно!
  }
}

В приведённом примере любое пользовательское значение напрямую вставляется в DOM, что создаёт опасность выполнения вредоносного скрипта.


Безопасная альтернатива: использование textContent

Для вставки текстовых данных безопаснее применять textContent, который интерпретирует содержимое как текст, а не HTML:

updateContentSafe() {
  const userInput = "<script>alert('XSS')</script>";
  this.outputTarget.textContent = userInput; // Отображается как текст
}

Результат отображается именно как строка, а не исполняемый код, полностью исключая XSS-риски.


Создание безопасного HTML с DOM API

Если необходимо вставлять HTML, но с контролем над безопасностью, предпочтительно использовать методы DOM API, такие как createElement и appendChild:

updateContentWithDOM() {
  const container = this.outputTarget;
  container.innerHTML = ""; // Очистка перед добавлением новых элементов

  const paragraph = document.createElement("p");
  paragraph.textContent = "Безопасный текст";
  container.appendChild(paragraph);

  const link = document.createElement("a");
  link.href = "#";
  link.textContent = "Ссылка";
  container.appendChild(link);
}

Такой подход гарантирует, что пользовательские данные никогда не станут исполняемым кодом.


Использование шаблонов <template> для безопасного HTML

HTML-шаблоны позволяют хранить разметку вне DOM, а затем клонировать её и вставлять безопасным образом:

<div data-controller="example">
  <div data-example-target="output"></div>

  <template id="message-template">
    <p class="message"></p>
  </template>
</div>
updateFromTemplate(userInput) {
  const template = document.getElementById("message-template");
  const clone = template.content.cloneNode(true);

  clone.querySelector(".message").textContent = userInput;
  this.outputTarget.appendChild(clone);
}

Такой способ обеспечивает полный контроль над содержимым, предотвращая выполнение встроенных скриптов.


Фильтрация пользовательских данных

Если вставка HTML неизбежна, данные нужно фильтровать. Для этого можно использовать сторонние библиотеки, например DOMPurify, которая удаляет опасные элементы и атрибуты:

import DOMPurify from "dompurify";

updateWithPurify(userInput) {
  const cleanHTML = DOMPurify.sanitize(userInput);
  this.outputTarget.innerHTML = cleanHTML;
}

DOMPurify позволяет безопасно вставлять HTML, сохраняя нужное форматирование и удаляя потенциально опасные скрипты.


Практические рекомендации

  • Никогда не присваивать пользовательский ввод напрямую innerHTML.
  • Использовать textContent для текстовой информации.
  • Для сложной разметки применять createElement, appendChild или шаблоны <template>.
  • При необходимости работы с HTML использовать проверенные библиотеки для санитайзинга, например DOMPurify.
  • Очистка контейнера перед добавлением новых элементов помогает избегать накопления нежелательных тегов и скриптов.

Безопасная работа с innerHTML в Stimulus требует дисциплины в обработке данных, строгого разделения текста и HTML и использования встроенных инструментов для контроля структуры DOM. Такой подход предотвращает уязвимости и обеспечивает надёжную работу динамических интерфейсов.