Cайт веб-разработчика, программиста Ruby on Rails ESV Corp. Екатеринбург, Москва, Санкт-Петербург, Новосибирск, Первоуральск

Использование С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- .