Безопасное оформление динамического контента из CMS
Системы управления контентом позволяют редакторам легко публиковать форматированный контент, но создаваемый ими HTML может быть непредсказуемым и небезопасным. В этой статье описаны практические подходы для поддержания согласованности дизайна при одновременной защите пользователей и вашего приложения.
Почему это сложно
HTML, созданный CMS, может содержать неожиданные теги, встроенные стили, имена классов, конфликтующие с вашими CSS, поврежденную разметку или даже вредоносные скрипты. Без контроля редакционная свобода может нарушить расположение элементов, обойти дизайн-токены или создать уязвимости XSS.
Основные принципы
- Очистка: удаляйте или преобразуйте небезопасные разметку и атрибуты перед отображением.
- Область действия: ограничьте влияние контента CMS на глобальные стили и наоборот.
- Структура: предпочтительно моделировать содержимое как структурированные компоненты (богатый текст, переносимый текст или кастомный JSON), а не как raw HTML.
- Политика: внедряйте политику безопасности контента (CSP) и проверяйте загрузки/ссылки.
- Тестирование и мониторинг: проверяйте регрессии стилей и безопасность через CI и проверки в рантайме.
Очистка и возможность белого списка
Никогда не вставляйте raw HTML в DOM без очистки. Используйте известную библиотеку на сервере или во время рендеринга, чтобы применить строгий список разрешенных тегов и атрибутов. Популярные варианты:
- DOMPurify (браузер / Node)
- sanitize-html (Node)
- Bleach (Python)
Пример (DOMPurify):
// Использование DOMPurify для удаления скриптов и неожиданных атрибутов
var clean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['p','br','strong','em','a','ul','ol','li','img','h2','h3','h4','table','thead','tbody','tr','th','td'],
ALLOWED_ATTR: ['href','src','alt','title','width','height','target','rel']
});
document.querySelector('.cms-content').innerHTML = clean;
Примечания:
- Запрещайте встроенные обработчики событий (onclick) и атрибуты стилей, если хотите централизовать оформление.
- Всегда удаляйте теги <script> и <style> из редакторов, не прошедших очистку.
Предпочтение структурированному содержимому вместо raw HTML
По возможности моделируйте содержимое в виде структурированных блоков (блоки с богатым текстом, переносимый текст или пользовательский JSON) и отображайте их через компоненты. Это исключает большинство сюрпризов и облегчает отображение с единым стилем и поведением.
Пример паттерна:
- CMS сохраняет блоки как {type: 'paragraph', text: '...'} или {type: 'image', src: '...'}.
- Ваш рендерер сопоставляет каждый тип с безопасным и стилизованным компонентом.
Область действия CSS и изоляция
Даже очищенная разметка может ввести классы и элементы, конфликтующие с вашими стилями. Используйте один или несколько вариантов для визуального ограничения контента CMS:
- Обертка: Добавляйте префикс всем стилям CMS — например, .cms-content. Это самый простой способ.
- Обнуление стилей внутри контейнера: обнулите отступы, размеры шрифтов и наследуйте дизайн-токены, чтобы снизить сюрпризы.
- Shadow DOM или iframe: полная изоляция с помощью Shadow DOM или iframe предотвращает утечки стилей. Используйте для недоверенных или очень изменяемых данных, учитывайте доступность и размер.
- Компоненты отображения: при использовании структурированного содержимого компоненты управляют разметкой и стилями, убирая необходимость стилизации произвольного пользовательского HTML.
Пример CSS для ограниченного окружения с сбросом и применением токенов:
/* Область стилей для CMS и нормализация базовых настроек */
.cms-content { --type-scale-1: 1rem; --type-scale-2: 1.125rem; color: #0f1724; font-family: system-ui, sans-serif; }
.cms-content * { box-sizing: border-box; }
.cms-content h2, .cms-content h3, .cms-content h4 { margin: 1rem 0 0.5rem; }
.cms-content p { margin: 0 0 1rem; font-size: var(--type-scale-1); }
.cms-content img { max-width: 100%; height: auto; display: block; margin: 0.5rem 0; }
.cms-content table { width: 100%; border-collapse: collapse; }
.cms-content table th, .cms-content table td { border: 1px solid #e6eef8; padding: 0.5rem; }
Если нужна почти полная изоляция, пример с Shadow DOM (очистить предварительно):
// Создайте теневой корень и вставьте безопасную HTML-разметку и стили
const host = document.getElementById('cms-host');
const shadow = host.attachShadow({ mode: 'open' });
const styles = `
:host { font-family: system-ui, sans-serif; color: #111; }
p { margin: 0 0 1rem; }
`;
shadow.innerHTML = `<style>${styles}</style><div class="cms-inner">${clean}

