Использование Сontent Security Policy на сайте Ruby on Rails: некоторые особенности реализации
Настроил использование механизма Сontent Security Policy (CSP) на сайтах Ruby on Rails - очень хорошо работает для противодействия XSS атакам и всякого рода code injections, при чём это современный механизм, который работает на уровне стандартов, на уровне браузеров, а не какое-то изобретение команды RoR. Директивы CSP хорошо описаны на Mozilla MDN.
Работает на уровне всего приложения (сайта), поэтому имеет смысл определить единый файл конфигурации в config/initializers/content_security_policy.rb:
Rails.application.config.content_security_policy do | policy |
policy.default_src :self
policy.script_src :self # :unsafe_inline - небезопасные inline-скрипты
# :strict_dynamic - скрипты, которые могут загружать другие скрипты, но мы им доверяем
# :unsafe_eval - небезопасные, непроверенные, но иногда вынужденно разрешаем
policy.style_src :self, :unsafe_inline # иногда используются inline-стили, это безопасно, сами себе доверяем
policy.img_src :self, :data, :blob
policy.font_src :self, :data
policy.frame_src :self
policy.frame_ancestors :self
policy.connect_src :self # AJAX-запросы только на наш сервер
# соединения для загрузки файлов/изображений
policy.base_uri :self
policy.form_action :self
policy.worker_src :self, :blob
policy.media_src :self
policy.object_src :none
end
Rails.application.config.content_security_policy_nonce_directives = %w(script-src style-src)
# auto add nonce - useful for 3rd-party scripts
Rails.application.config.content_security_policy_nonce_auto = true
# Rails.application.config.content_security_policy_nonce_generator = -> (request) { SecureRandom.base64(16) }
# самый простейший вариант генерации nonce, чтобы был единым для всего сайта, для всех запросов
Rails.application.config.content_security_policy_nonce_generator = -> (request) { request.session.id.to_s }
И далее начинаются интересности. В контроллере вы можете переопределить некоторые директивы конкретно для вашего контроллера или для действия:
class ApplicationController < ActionController::Base
# отключить CSP для всего контроллера
content_security_policy false
# отключить CSP для index
content_security_policy false, only: :index
# переопределить некоторые директивы для всего контроллера
content_security_policy do | policy |
# немного ослабим контроль
policy.script_src :self, :strict_dynamic
end
# либо то же самое, но для отдельного действия
content_security_policy(only: :index) do | policy |
policy.script_src :self, :strict_dynamic
end
end
Моё собственное наблюдение: вызывается метод content_security_policy_nonce контроллера — можно предположить, что для получения nonce, которое подставляется в скрипты и стили. Именно так и происходит, но есть ньюанс...
Отдельного внимания заслуживает
Rails.application.config.content_security_policy_nonce_generator
— вот тут происходят некоторые чудеса (It's not a bug, it's a feature). Когда он установлен, то в заголовок ответа сервера прописывается nonce — тут всё по-честному, но если вам нужен отдельный nonce для каждого контроллера — вот тут и начинаются "танцы с бубнами". К тому же, если этот generator не определён, то RoR вообще не прописывает nonce в заголовок ответа сервера.
Дело в том, что для подстановки в заголовок ответа сервера используется request.content_security_policy_nonce, который в свою очередь вычисляется только при наличии Rails.application.config.content_security_policy_nonce_generator, а вот в тэги script и style подставляется то, что возвращает метод content_security_policy_nonce контроллера, а они должны совпадать.
Получается, что используются несколько разные механизмы для получения nonce. Довольно странно всё это выглядит, ну да ладно...
Я предпочёл свой вариант:
# отключаем эту нелепую двусмысленность
Rails.application.config.content_security_policy_nonce_generator = nil
# в контроллере
class ApplicationController < ActionController::Base
# простейший вариант просто для примера
def content_security_policy_nonce = request.session.id.to_s
# политики безопасности для контроллера
content_security_policy do | policy |
# в данном случае о nonce в заголовке позаботится сам RoR
if Rails.application.config.content_security_policy_nonce_generator
policy.script_src :self
else
# It's not a bug, it's a feature ;-) )))
# при неустановленном Rails.application.config.content_security_policy_nonce_generator
# производится обращение к методу content_security_policy_nonce, но Rails всё-равно не вставляет nonce-
# поэтому позаботимся об этом сами
ps = policy.script_src
unless ps&.is_a?(Array) && ps.any? { it.to_s.include?('nonce') }
policy.script_src :self, "'nonce-#{content_security_policy_nonce}'"
end
end
end
end
Вся опасность заключается в том, что можно положиться на надёжность и разумность Ruby on Rails - прописать все политики — в исходниках страницы всё выглядит очень даже красиво - все nonce у скриптов и стилей заполенены, даже meta csp присутствует, но!... всё это не имеет никакого смысла и механизм CSP не работает, если в заголовке ответа сервера в разделе CSP нет этого заветного nonce- .