Spec-Zone.ru › htmx

hx-csp

Обеспечьте работу htmx при строгой политике безопасности содержимого

Расширение hx-csp обеспечивает применение политики безопасности содержимого для htmx на трёх уровнях: проверка nonce для предотвращения внедрения HTML, интеграция с Trusted Types для ограничения записи в DOM и безопасное выполнение eval, позволяющее исключить unsafe-eval из CSP.

Установка

<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-csp.js"></script>

Атрибут hx-nonce

Атрибут hx-nonce позволяет серверу сообщить расширению, что элемент htmx был намеренно сформирован доверенным серверным кодом. При инициализации элемента htmx расширение проверяет, совпадает ли его hx-nonce с nonce страницы: если значения не совпадают или атрибут отсутствует, все атрибуты htmx удаляются, и элемент становится неактивным.

Шаблонизатор должен добавлять hx-nonce ко всем элементам с атрибутами htmx:

<!-- Django -->
<button hx-post="/save" hx-nonce="{{ request.csp_nonce }}">Save</button>

<!-- Rails ERB -->
<button hx-post="/save" hx-nonce="<%= content_security_policy_nonce %>">Save</button>

<!-- Laravel Blade -->
<button hx-post="/save" hx-nonce="{{ csp_nonce() }}">Save</button>

Важно: сам по себе hx-nonce не гарантирует защиту от XSS. Он подтверждает, что элемент был сформирован сервером, но ничего не говорит о безопасности значений атрибутов этого элемента. Любые атрибуты htmx, содержащие данные, предоставленные пользователем, по-прежнему необходимо корректно экранировать в шаблонизаторе:

<!-- UNSAFE — user data in hx-get without escaping -->
<div hx-get="/search?q={{ user_input }}" hx-nonce="...">...</div>

<!-- SAFE — escaped by the templating engine -->
<div hx-get="/search?q={{ user_input | escape }}" hx-nonce="...">...</div>

Все основные шаблонизаторы по умолчанию экранируют HTML-сущности (", ', <, >, &) — убедитесь, что вы не обходите это поведение с помощью фильтров вывода необработанных/неэкранированных данных (| raw, | safe, raw() и т. д.), которые позволят " во вводе пользователя выйти за пределы атрибута и внедрить новые атрибуты.

Расширение также блокирует атрибуты со значениями URL (hx-get, hx-post и т. д.), если значение является URI схемы js: или javascript: — такие схемы сохраняются после кодирования сущностей и не являются допустимыми URL действий.

Никогда не помещайте данные, контролируемые пользователем, в атрибуты, значения которых вычисляются как JS. hx-on:*, hx-vals js:, hx-confirm js: и фильтры событий запуска вычисляются как JavaScript. Экранирование HTML здесь не поможет: браузер декодирует HTML-сущности до передачи строки движку JS, поэтому злоумышленник может выйти за пределы строкового литерала, даже если шаблонизатор экранировал значение.

В большинстве языков шаблонов нет стандартного фильтра для экранирования строк JavaScript. Даже в тех фреймворках, где такой фильтр есть (escapejs в Django, escape_javascript в Rails), его необходимо правильно применять каждый раз, и он может не защитить от всех способов внедрения в контексте JS.

Единственный безопасный подход — вовсе не помещать пользовательские данные в атрибут, а передавать их через data-*:

<!-- UNSAFE — HTML escaping does not protect JS string literals -->
<button hx-on:click="doThing('{{ user.name }}')" hx-nonce="...">

<!-- SAFE — user data never appears in JS source -->
<button data-name="{{ user.name }}"
        hx-on:click="doThing(this.dataset.name)"
        hx-nonce="...">

Настройте заголовок Content-Security-Policy с nonce для каждого запроса и добавляйте hx-nonce ко всем элементам htmx:

Content-Security-Policy: script-src 'self' 'nonce-<nonce>'
<script nonce="<nonce>" src="/htmx.js"></script>
<script nonce="<nonce>" src="/hx-csp.js"></script>

<button hx-post="/save" hx-nonce="<nonce>">Save</button>

Расширение считывает nonce страницы из первого элемента script[nonce] и блокирует любой элемент htmx, у которого hx-nonce не совпадает. Если nonce страницы не найден, вся обработка htmx блокируется — по умолчанию доступ запрещён.

Trusted Types

Загрузка hx-csp автоматически создаёт политику Trusted Types 'htmx'. Добавьте её в CSP, чтобы гарантировать, что записывать HTML в точки вставки DOM может только htmx:

Content-Security-Policy: script-src 'self' 'nonce-<nonce>';
                         require-trusted-types-for 'script';
                         trusted-types htmx

Дополнительная настройка не требуется. В браузерах без поддержки Trusted Types расширение работает прозрачно. Если 'htmx' не внесён в список разрешённых значений trusted-types, вся обработка htmx блокируется — по умолчанию доступ запрещён.

Безопасный eval

Возможности htmx для вычисления JS-выражений (hx-on:, hx-vals js:, hx-confirm js:, фильтры событий запуска) полностью необязательны. Если вы ими не пользуетесь, можно вовсе исключить unsafe-eval из CSP без дополнительных настроек.

Если вы используете эти возможности, задайте safeEval:true, чтобы заменить new Function() в htmx на внедрение скрипта с использованием nonce и включить их без unsafe-eval:

Content-Security-Policy: script-src 'self' 'nonce-<nonce>'
<meta name="htmx-config" content='extensions:"hx-csp",safeEval:true'>

Частичные ответы

Частичные ответы должны содержать заголовок Content-Security-Policy с новым nonce. Перед разбором фрагмента расширение заменяет nonce ответа на nonce страницы, чтобы добавленные элементы прошли последующие проверки nonce:

Content-Security-Policy: script-src 'nonce-<response-nonce>'

Защита от повторного использования nonce

Расширение заменяет nonce ответа на nonce страницы, чтобы добавленные элементы прошли последующие проверки nonce. Перед заменой оно очищает из исходного текста ответа все элементы, уже содержащие значение nonce страницы.

Сервер не может знать nonce страницы — ему известен только собственный nonce для каждого ответа. Поэтому если nonce страницы появляется в ответе, его добавил злоумышленник, а не сервер. Предварительная очистка гарантирует, что при замене элементы, контролируемые злоумышленником, не получат доверенный статус случайно.

Риск: в отличие от <script nonce>, атрибуты hx-nonce не очищаются браузерами после разбора и поэтому могут стать дополнительным источником утечки nonce. Очистка — это дополнительная мера защиты, которая не позволяет заранее добавить украденный nonce во внедрённый контент для прохождения проверок nonce.

Кэширование ответов с nonce

Если вы кэшируете ответы с nonce, необходимо обеспечить раздельное кэширование первоначальной загрузки страницы и последующих запросов htmx. В противном случае защита от повторного использования nonce заблокирует допустимые ответы: когда пользователь возвращается на кэшированную страницу с помощью восстановления истории, кэшированный ответ будет содержать тот же nonce, что и текущая страница, что запустит очистку.

В каждом запросе htmx отправляет заголовок HX-Request-Type (full или partial). Добавьте Vary: HX-Request-Type, чтобы разделить кэш:

Cache-Control: public, max-age=3600
Vary: HX-Request-Type

Это обеспечивает раздельное кэширование первоначальной загрузки страницы и запросов htmx, поэтому nonce ответа всегда будет отличаться от nonce страницы.

Встроенные скрипты в заменяемом содержимом

Когда htmx вставляет HTML, содержащий теги <script>, он создаёт их заново, чтобы запустить выполнение. Расширение hx-csp гарантирует, что nonce ответа будет заменён на nonce страницы до разбора, благодаря чему nonce скриптов корректно обновляются и скрипты выполняются при строгой политике script-src 'nonce-<nonce>'.

Это оптимальная замена для htmx.config.inlineScriptNonce. Не используйте inlineScriptNonce вместе с этим расширением: оно назначает один статический nonce всем вставленным скриптам независимо от их источника, что подрывает модель nonce для каждого ответа, обеспечиваемую этим расширением.

События безопасности

Событие Когда возникает
htmx:security:strip Элемент очищен из-за отсутствующего или несовпадающего nonce
htmx:security:violation Несовпадение nonce во время eval, отправитель формы с включённым режимом повышения без nonce или URL действия js:/javascript:
document.addEventListener('htmx:security:strip', e => {
    console.warn(e.target, e.detail.reason, e.detail.stripped);
});

detail.reason — это 'missing-nonce', 'nonce-mismatch' или 'javascript-url'.

Licensed under the Zero-Clause BSD License.
https://four.htmx.org/extensions/hx-csp/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API