]> git.openstreetmap.org Git - rails.git/commitdiff
Switch to using rails builtin content security policy support
authorTom Hughes <tom@compton.nu>
Thu, 21 Mar 2024 20:32:12 +0000 (20:32 +0000)
committerTom Hughes <tom@compton.nu>
Wed, 22 May 2024 15:38:59 +0000 (16:38 +0100)
16 files changed:
Gemfile
Gemfile.lock
app/controllers/accounts_controller.rb
app/controllers/application_controller.rb
app/controllers/diary_entries_controller.rb
app/controllers/export_controller.rb
app/controllers/messages_controller.rb
app/controllers/oauth2_authorizations_controller.rb
app/controllers/oauth_controller.rb
app/controllers/sessions_controller.rb
app/controllers/site_controller.rb
app/controllers/users_controller.rb
app/views/layouts/_head.html.erb
config/initializers/content_security_policy.rb
config/initializers/secure_headers.rb [deleted file]
config/initializers/session_store.rb

diff --git a/Gemfile b/Gemfile
index 75387b5d5e3b5c8db060e427ca352e834c6b6014..77a9f1c97ff252c30ce1cd4f66dbd1034c1307ec 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -116,9 +116,6 @@ gem "connection_pool"
 gem "dalli"
 gem "kgio"
 
-# Load secure_headers for Content-Security-Policy support
-gem "secure_headers"
-
 # Load canonical-rails to generate canonical URLs
 gem "canonical-rails"
 
index 23518339bf75b7644df01923eb8af70ed07a9e9a..bf78a3d3d181d2130ec4ee1436b83ad83af866ff 100644 (file)
@@ -529,7 +529,6 @@ GEM
     sass-embedded (1.64.2)
       google-protobuf (~> 3.23)
       rake (>= 13.0.0)
-    secure_headers (6.5.0)
     selenium-webdriver (4.21.1)
       base64 (~> 0.2)
       rexml (~> 3.2, >= 3.2.5)
@@ -690,7 +689,6 @@ DEPENDENCIES
   rubocop-rake
   sanitize
   sass-embedded (~> 1.64.0)
-  secure_headers
   selenium-webdriver
   simplecov
   simplecov-lcov
index 6b47ca6660d1ee200691f05b0443178eb1d54997..d45dce66a8e00f6f413831d767436b8b537524e2 100644 (file)
@@ -11,15 +11,13 @@ class AccountsController < ApplicationController
 
   before_action :check_database_readable
   before_action :check_database_writable, :only => [:update]
-  before_action :allow_thirdparty_images, :only => [:edit, :update]
+
+  allow_thirdparty_images :only => [:edit, :update]
+  allow_social_login :only => [:edit, :update]
 
   def edit
     @tokens = current_user.oauth_tokens.authorized
 
-    append_content_security_policy_directives(
-      :form_action => %w[accounts.google.com *.facebook.com login.microsoftonline.com github.com meta.wikimedia.org]
-    )
-
     if errors = session.delete(:user_errors)
       errors.each do |attribute, error|
         current_user.errors.add(attribute, error)
@@ -31,10 +29,6 @@ class AccountsController < ApplicationController
   def update
     @tokens = current_user.oauth_tokens.authorized
 
-    append_content_security_policy_directives(
-      :form_action => %w[accounts.google.com *.facebook.com login.microsoftonline.com github.com meta.wikimedia.org]
-    )
-
     user_params = params.require(:user).permit(:display_name, :new_email, :pass_crypt, :pass_crypt_confirmation, :auth_provider)
 
     if params[:user][:auth_provider].blank? ||
index f5accc3c44d2d65654105a828d5ccc0fc6e94b64..05fa76658d55bedeecf412c4a5d1927cb6ce3899 100644 (file)
@@ -13,13 +13,30 @@ class ApplicationController < ActionController::Base
   rescue_from RailsParam::InvalidParameterError, :with => :invalid_parameter
 
   before_action :fetch_body
-  around_action :better_errors_allow_inline, :if => proc { Rails.env.development? }
 
   attr_accessor :current_user, :oauth_token
 
   helper_method :current_user
   helper_method :oauth_token
 
+  def self.allow_thirdparty_images(**options)
+    content_security_policy(options) do |policy|
+      policy.img_src("*")
+    end
+  end
+
+  def self.allow_social_login(**options)
+    content_security_policy(options) do |policy|
+      policy.form_action(*policy.form_action, "accounts.google.com", "*.facebook.com", "login.microsoftonline.com", "github.com", "meta.wikimedia.org")
+    end
+  end
+
+  def self.allow_all_form_action(**options)
+    content_security_policy(options) do |policy|
+      policy.form_action(nil)
+    end
+  end
+
   private
 
   def authorize_web
@@ -233,13 +250,15 @@ class ApplicationController < ActionController::Base
   end
 
   def map_layout
-    append_content_security_policy_directives(
-      :child_src => %w[http://127.0.0.1:8111 https://127.0.0.1:8112],
-      :frame_src => %w[http://127.0.0.1:8111 https://127.0.0.1:8112],
-      :connect_src => [Settings.nominatim_url, Settings.overpass_url, Settings.fossgis_osrm_url, Settings.graphhopper_url, Settings.fossgis_valhalla_url],
-      :form_action => %w[render.openstreetmap.org],
-      :style_src => %w['unsafe-inline']
-    )
+    policy = request.content_security_policy.clone
+
+    policy.child_src(*policy.child_src, "http://127.0.0.1:8111", "https://127.0.0.1:8112")
+    policy.frame_src(*policy.frame_src, "http://127.0.0.1:8111", "https://127.0.0.1:8112")
+    policy.connect_src(*policy.connect_src, Settings.nominatim_url, Settings.overpass_url, Settings.fossgis_osrm_url, Settings.graphhopper_url, Settings.fossgis_valhalla_url)
+    policy.form_action(*policy.form_action, "render.openstreetmap.org")
+    policy.style_src(*policy.style_src, :unsafe_inline)
+
+    request.content_security_policy = policy
 
     case Settings.status
     when "database_offline", "api_offline"
@@ -251,10 +270,6 @@ class ApplicationController < ActionController::Base
     request.xhr? ? "xhr" : "map"
   end
 
-  def allow_thirdparty_images
-    append_content_security_policy_directives(:img_src => %w[*])
-  end
-
   def preferred_editor
     if params[:editor]
       params[:editor]
@@ -277,17 +292,6 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def better_errors_allow_inline
-    yield
-  rescue StandardError
-    append_content_security_policy_directives(
-      :script_src => %w['unsafe-inline'],
-      :style_src => %w['unsafe-inline']
-    )
-
-    raise
-  end
-
   def current_ability
     Ability.new(current_user)
   end
index 8da0842eb09cbee7942cf28d725d5ee6b49e9308..9af36709eb20be1a2055a964daf2697d3443af1f 100644 (file)
@@ -12,7 +12,8 @@ class DiaryEntriesController < ApplicationController
 
   before_action :lookup_user, :only => [:show, :comments]
   before_action :check_database_writable, :only => [:new, :create, :edit, :update, :comment, :hide, :hidecomment, :subscribe, :unsubscribe]
-  before_action :allow_thirdparty_images, :only => [:new, :create, :edit, :update, :index, :show, :comments]
+
+  allow_thirdparty_images :only => [:new, :create, :edit, :update, :index, :show, :comments]
 
   def index
     if params[:display_name]
index 94851de96485741758d681a1247be029c402dc17..cddc97b6883e13044601c5b4a3a24b1f84d83f74 100644 (file)
@@ -4,6 +4,10 @@ class ExportController < ApplicationController
   before_action :update_totp, :only => [:finish]
   authorize_resource :class => false
 
+  content_security_policy(:only => :embed) do |policy|
+    policy.frame_ancestors("*")
+  end
+
   caches_page :embed
 
   # When the user clicks 'Export' we redirect to a URL which generates the export download
@@ -25,9 +29,5 @@ class ExportController < ApplicationController
     end
   end
 
-  def embed
-    append_content_security_policy_directives(
-      :frame_ancestors => %w[*]
-    )
-  end
+  def embed; end
 end
index adfbd9157992537c18c35b35beb1c67513dbef03..779174e255ffc915fe14f44c67e2710b8cb87cba 100644 (file)
@@ -11,7 +11,8 @@ class MessagesController < ApplicationController
   before_action :lookup_user, :only => [:new, :create]
   before_action :check_database_readable
   before_action :check_database_writable, :only => [:new, :create, :reply, :mark, :destroy]
-  before_action :allow_thirdparty_images, :only => [:new, :create, :show]
+
+  allow_thirdparty_images :only => [:new, :create, :show]
 
   # Show a message
   def show
index dca95de4e7a34b737906ba2dd524d1c7b2e3f49f..415ab2775c12f248e90598bde7e2495078e126f9 100644 (file)
@@ -3,13 +3,8 @@ class Oauth2AuthorizationsController < Doorkeeper::AuthorizationsController
 
   prepend_before_action :authorize_web
   before_action :set_locale
-  before_action :allow_all_form_action, :only => [:new]
 
-  authorize_resource :class => false
-
-  private
+  allow_all_form_action :only => :new
 
-  def allow_all_form_action
-    override_content_security_policy_directives(:form_action => []) if Settings.csp_enforce || Settings.key?(:csp_report_url)
-  end
+  authorize_resource :class => false
 end
index 49af05b0d188e814960468d0a0f335cfd989f62e..62a68b53369ddc0e2cf4829a9a4c40f4d5f552d5 100644 (file)
@@ -9,6 +9,8 @@ class OauthController < ApplicationController
 
   layout "site"
 
+  allow_all_form_action :only => :oauth1_authorize
+
   def revoke
     @token = current_user.oauth_tokens.find_by :token => params[:token]
     if @token
@@ -41,8 +43,6 @@ class OauthController < ApplicationController
   end
 
   def oauth1_authorize
-    override_content_security_policy_directives(:form_action => []) if Settings.csp_enforce || Settings.key?(:csp_report_url)
-
     if @token.invalidated?
       @message = t "oauth.authorize_failure.invalid"
       render :action => "authorize_failure"
index 2b6905ebb548fc9f2703d3045c0f3c282f74dfe4..a3e6f42f03db4b172607bc26285d6c79c3b0ee8b 100644 (file)
@@ -11,9 +11,9 @@ class SessionsController < ApplicationController
 
   authorize_resource :class => false
 
-  def new
-    override_content_security_policy_directives(:form_action => []) if Settings.csp_enforce || Settings.key?(:csp_report_url)
+  allow_all_form_action :only => :new
 
+  def new
     referer = safe_referer(params[:referer]) if params[:referer]
 
     parse_oauth_referer referer
index 3d830c63f7fab352aa47b893a7c3fdfa0d9a9e07..172be56531df7e444e4c7ed3b94236480378c660 100644 (file)
@@ -12,6 +12,17 @@ class SiteController < ApplicationController
 
   authorize_resource :class => false
 
+  content_security_policy(:only => :edit) do |policy|
+    policy.frame_src(*policy.frame_src, :blob)
+  end
+
+  content_security_policy(:only => :id) do |policy|
+    policy.connect_src("*")
+    policy.img_src("*", :blob)
+    policy.script_src(*policy.script_src, "dev.virtualearth.net", :unsafe_eval)
+    policy.style_src(*policy.style_src, :unsafe_inline)
+  end
+
   def index
     session[:location] ||= OSM.ip_location(request.env["REMOTE_ADDR"]) unless Settings.status == "database_readonly" || Settings.status == "database_offline"
   end
@@ -71,12 +82,6 @@ class SiteController < ApplicationController
       require_user
     end
 
-    if %w[id].include?(editor)
-      append_content_security_policy_directives(
-        :frame_src => %w[blob:]
-      )
-    end
-
     begin
       if params[:node]
         bbox = Node.visible.find(params[:node]).bbox.to_unscaled
@@ -136,13 +141,6 @@ class SiteController < ApplicationController
   end
 
   def id
-    append_content_security_policy_directives(
-      :connect_src => %w[*],
-      :img_src => %w[* blob:],
-      :script_src => %w[dev.virtualearth.net 'unsafe-eval'],
-      :style_src => %w['unsafe-inline']
-    )
-
     render :layout => false
   end
 
index f3ba5df2c2f5a99c7855d04c5bd564a3466d3c02..186e06120e0f70de52db61d112ec88c82240b9c5 100644 (file)
@@ -17,7 +17,9 @@ class UsersController < ApplicationController
   before_action :check_database_writable, :only => [:new, :go_public]
   before_action :require_cookies, :only => [:new]
   before_action :lookup_user_by_name, :only => [:set_status, :destroy]
-  before_action :allow_thirdparty_images, :only => [:show]
+
+  allow_thirdparty_images :only => :show
+  allow_social_login :only => :new
 
   ##
   # display a list of users matching specified criteria
@@ -58,10 +60,6 @@ class UsersController < ApplicationController
 
     parse_oauth_referer @referer
 
-    append_content_security_policy_directives(
-      :form_action => %w[accounts.google.com *.facebook.com login.microsoftonline.com github.com meta.wikimedia.org]
-    )
-
     if current_user
       # The user is logged in already, so don't show them the signup
       # page, instead send them to the home page
index 724ca552635e4582313d043070a85b3077a5faa7..3c691612ab1d4c37ec1ad7a0f8c001b334e50272 100644 (file)
@@ -12,6 +12,6 @@
   <%= yield :head %>
   <%= yield :auto_discovery_link_tag %>
   <%= csrf_meta_tag %>
-  <meta name="csp-nonce" content="<%= content_security_policy_style_nonce %>" />
+  <%= csp_meta_tag %>
   <title><%= "#{@title} | " if @title %><%= t "layouts.project_name.title" %></title>
 <% end %>
index b3076b38fe14399a56099ba187b1cb21cac15f09..5ce53e86360117b4f320d7e8f84d4da09edb2769 100644 (file)
@@ -4,22 +4,42 @@
 # See the Securing Rails Applications Guide for more information:
 # https://guides.rubyonrails.org/security.html#content-security-policy-header
 
-# Rails.application.configure do
-#   config.content_security_policy do |policy|
-#     policy.default_src :self, :https
-#     policy.font_src    :self, :https, :data
-#     policy.img_src     :self, :https, :data
-#     policy.object_src  :none
-#     policy.script_src  :self, :https
-#     policy.style_src   :self, :https
-#     # Specify URI for violation reports
-#     # policy.report_uri "/csp-violation-report-endpoint"
-#   end
-#
-#   # Generate session nonces for permitted importmap, inline scripts, and inline styles.
-#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
-#   config.content_security_policy_nonce_directives = %w(script-src style-src)
-#
-#   # Report violations without enforcing the policy.
-#   # config.content_security_policy_report_only = true
-# end
+Rails.application.configure do
+  connect_src = [:self]
+  img_src = [:self, :data, "www.gravatar.com", "*.wp.com", "tile.openstreetmap.org", "*.tile.thunderforest.com", "tile.tracestrack.com", "*.openstreetmap.fr"]
+  script_src = [:self]
+
+  connect_src << Settings.matomo["location"] if defined?(Settings.matomo)
+  img_src << Settings.matomo["location"] if defined?(Settings.matomo)
+  script_src << Settings.matomo["location"] if defined?(Settings.matomo)
+
+  img_src << Settings.avatar_storage_url if Settings.key?(:avatar_storage_url)
+  img_src << Settings.trace_image_storage_url if Settings.key?(:trace_image_storage_url)
+
+  config.content_security_policy do |policy|
+    policy.default_src :self
+    policy.child_src(:self)
+    policy.connect_src(*connect_src)
+    policy.font_src(:none)
+    policy.form_action(:self)
+    policy.frame_ancestors(:self)
+    policy.frame_src(:self)
+    policy.img_src(*img_src)
+    policy.manifest_src(:self)
+    policy.media_src(:none)
+    policy.object_src(:self)
+    policy.plugin_types
+    policy.script_src(*script_src)
+    policy.style_src(:self)
+    policy.worker_src(:none)
+    policy.manifest_src(:self)
+    policy.report_uri(Settings.csp_report_url) if Settings.key?(:csp_report_url)
+  end
+
+  # Generate session nonces for permitted importmap and inline scripts
+  config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(24) }
+  config.content_security_policy_nonce_directives = %w[style-src]
+
+  # Report violations without enforcing the policy.
+  config.content_security_policy_report_only = true unless Settings.csp_enforce
+end
diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb
deleted file mode 100644 (file)
index 60f1551..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-csp_policy = {
-  :preserve_schemes => true,
-  :default_src => %w['self'],
-  :child_src => %w['self'],
-  :connect_src => %w['self'],
-  :font_src => %w['none'],
-  :form_action => %w['self'],
-  :frame_ancestors => %w['self'],
-  :frame_src => %w['self'],
-  :img_src => %w['self' data: www.gravatar.com *.wp.com tile.openstreetmap.org *.tile.openstreetmap.org *.tile.thunderforest.com tile.tracestrack.com *.openstreetmap.fr],
-  :manifest_src => %w['self'],
-  :media_src => %w['none'],
-  :object_src => %w['self'],
-  :plugin_types => %w[],
-  :script_src => %w['self'],
-  :style_src => %w['self'],
-  :worker_src => %w['none'],
-  :report_uri => []
-}
-
-csp_policy[:connect_src] << Settings.matomo["location"] if defined?(Settings.matomo)
-csp_policy[:img_src] << Settings.matomo["location"] if defined?(Settings.matomo)
-csp_policy[:script_src] << Settings.matomo["location"] if defined?(Settings.matomo)
-
-csp_policy[:img_src] << Settings.avatar_storage_url if Settings.key?(:avatar_storage_url)
-csp_policy[:img_src] << Settings.trace_image_storage_url if Settings.key?(:trace_image_storage_url)
-
-csp_policy[:report_uri] << Settings.csp_report_url if Settings.key?(:csp_report_url)
-
-cookie_policy = {
-  :httponly => { :only => %w[_osm_session _osm_totp_token] }
-}
-
-SecureHeaders::Configuration.default do |config|
-  config.hsts = SecureHeaders::OPT_OUT
-  config.referrer_policy = "strict-origin-when-cross-origin"
-
-  if Settings.csp_enforce
-    config.csp = csp_policy
-    config.csp_report_only = SecureHeaders::OPT_OUT
-  elsif Settings.key?(:csp_report_url)
-    config.csp = SecureHeaders::OPT_OUT
-    config.csp_report_only = csp_policy
-  else
-    config.csp = SecureHeaders::OPT_OUT
-    config.csp_report_only = SecureHeaders::OPT_OUT
-  end
-
-  config.cookies = cookie_policy
-end
index a122848553bf1a9f00fe4d09f2f18ee292617f3b..4119064c39cf1601dbcf5babc877ff06ae953ae1 100644 (file)
@@ -1,7 +1,7 @@
 # Be sure to restart your server when you modify this file.
 
 if Settings.key?(:memcache_servers)
-  Rails.application.config.session_store :mem_cache_store, :memcache_server => Settings.memcache_servers, :namespace => "rails:session", :key => "_osm_session"
+  Rails.application.config.session_store :mem_cache_store, :memcache_server => Settings.memcache_servers, :namespace => "rails:session", :key => "_osm_session", :same_site => :lax
 else
-  Rails.application.config.session_store :cache_store, :key => "_osm_session", :cache => ActiveSupport::Cache::MemoryStore.new
+  Rails.application.config.session_store :cache_store, :key => "_osm_session", :cache => ActiveSupport::Cache::MemoryStore.new, :same_site => :lax
 end