diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ebaed00..521d880b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,7 @@ jobs: cp config/secrets.yml.ci config/secrets.yml cp config/docker.yml.erb.ci config/docker.yml.erb cp config/mnemosyne.yml.ci config/mnemosyne.yml + cp config/content_security_policy.yml.ci config/content_security_policy.yml - name: Create database env: diff --git a/.gitignore b/.gitignore index 1e2bbb2b..759d2f78 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /config/mnemosyne.yml /config/secrets.yml /config/docker.yml.erb +/config/content_security_policy.yml /coverage /log/*.* /public/assets diff --git a/app/views/exercises/feedback.html.slim b/app/views/exercises/feedback.html.slim index 23cdd608..e52b5c5b 100644 --- a/app/views/exercises/feedback.html.slim +++ b/app/views/exercises/feedback.html.slim @@ -36,4 +36,5 @@ h1 = link_to_if(policy(@exercise).show?, @exercise, exercise_path(@exercise)) = render('shared/pagination', collection: @feedbacks) - script type="text/javascript" $(function () { $('[data-bs-toggle="tooltip"]').tooltip() }); + = javascript_tag nonce: true do + | $(function () { $('[data-bs-toggle="tooltip"]').tooltip() }); diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 044088d1..bd59b19f 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -15,8 +15,8 @@ html lang="#{I18n.locale || I18n.default_locale}" = javascript_include_tag('application', 'data-turbolinks-track': true) = yield(:head) = csrf_meta_tags - = timeago_script_tag - script type="text/javascript" + = timeago_script_tag nonce: true + = javascript_tag nonce: true do | I18n.defaultLocale = "#{I18n.default_locale}"; | I18n.locale = "#{I18n.locale}"; - if SentryJavascript.active? diff --git a/app/views/request_for_comments/show.html.slim b/app/views/request_for_comments/show.html.slim index aaaf578a..b8a2acf6 100644 --- a/app/views/request_for_comments/show.html.slim +++ b/app/views/request_for_comments/show.html.slim @@ -79,7 +79,7 @@ = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') -javascript: +javascript [nonce=content_security_policy_nonce]: $('.modal-content').draggable({ handle: '.modal-header' diff --git a/config/content_security_policy.yml.ci b/config/content_security_policy.yml.ci new file mode 100644 index 00000000..312725e9 --- /dev/null +++ b/config/content_security_policy.yml.ci @@ -0,0 +1,18 @@ +default: &default + default_src: [] + + +development: + <<: *default + # Allow the webpack-dev-server in development + connect_src: + - http://localhost:3035 + - ws://localhost:3035 + + +production: + <<: *default + + +test: + <<: *default diff --git a/config/content_security_policy.yml.example b/config/content_security_policy.yml.example new file mode 100644 index 00000000..a766f1ac --- /dev/null +++ b/config/content_security_policy.yml.example @@ -0,0 +1,29 @@ +# This file allows to further customize the Content Security Policy (CSP) +# All settings will be applied **in addition** to the application CSP +# Default directives are defined here: `initializers/content_security_policy.rb` + +default: &default + # Allow the S3 service hosted by the openHPI Cloud to be used for images + img_src: + - https://s3.xopic.de + - https://*.s3.xopic.de + - https://s3.openhpicloud.de + - https://*.s3.openhpicloud.de + # Optionally: Specify a custom, non-Sentry URL for reporting CSP violations + # report_uri: https://example.com/csp-report + + +development: + <<: *default + # Allow the webpack-dev-server in development + connect_src: + - http://localhost:3035 + - ws://localhost:3035 + + +production: + <<: *default + + +test: + <<: *default diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index f87cb06b..07bc82ea 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -6,28 +6,57 @@ # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy -# Rails.application.config.content_security_policy do |policy| -# # If you are using webpack-dev-server then specify webpack-dev-server host -# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? +require_relative 'sentry_csp' +require_relative 'sentry_javascript' -# 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 -# # If you are using webpack-dev-server then specify webpack-dev-server host -# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? +def self.apply_yml_settings_for(policy) + csp_settings = CodeOcean::Config.new(:content_security_policy) -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end + csp_settings.read.each do |directive, additional_settings| + existing_settings = if directive == 'report_uri' + '' + else + policy.public_send(directive) || [] + end + all_settings = existing_settings + additional_settings + policy.public_send(directive, *all_settings) + end +end + +def self.apply_sentry_settings_for(policy) + sentry_domain = URI.parse SentryJavascript.dsn + additional_setting = "#{sentry_domain.scheme}://#{sentry_domain.host}" + existing_settings = policy.connect_src || [] + all_settings = existing_settings + [additional_setting] + policy.connect_src(*all_settings) +end + +Rails.application.config.content_security_policy do |policy| + policy.default_src :none + policy.base_uri :none + policy.font_src :self + # Code executions might return a base64 encoded image as a :data URI + policy.img_src :self, :data + policy.object_src :none + policy.script_src :self, :report_sample + # Our ACE editor unfortunately requires :unsafe_inline for the code highlighting + policy.style_src :self, :unsafe_inline, :report_sample + policy.connect_src :self + policy.form_action :self + policy.frame_ancestors :none + + # Specify URI for violation reports + policy.report_uri SentryCsp.report_url if SentryCsp.active? + + apply_yml_settings_for policy + apply_sentry_settings_for policy if SentryJavascript.active? +end # If you are using UJS then enable automatic nonce generation -# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } +Rails.application.config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # Set the nonce only to specific directives -# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) +Rails.application.config.content_security_policy_nonce_directives = %w[script-src] # Report CSP violations to a specified URI # For further information see the following documentation: diff --git a/config/initializers/sentry_csp.rb b/config/initializers/sentry_csp.rb new file mode 100644 index 00000000..891f5399 --- /dev/null +++ b/config/initializers/sentry_csp.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative 'sentry' + +class SentryCsp + def self.active? + dsn.present? && %w[development test].exclude?(environment) + end + + def self.report_url + parsed_url = URI.parse dsn + + # Add additional variables to the query string + query_params = CGI.parse(parsed_url.query || '') + query_params[:sentry_release] = release if release + query_params[:sentry_environment] = environment if environment + + # Add the query string back to the URL + parsed_url.query = URI.encode_www_form(query_params) + + # Return the full URL + parsed_url.to_s + end + + class << self + private + + def dsn + ENV.fetch('SENTRY_CSP_REPORT_URL', nil) + end + + def release + Sentry.configuration.release + end + + def environment + Sentry.configuration.environment + end + end +end diff --git a/config/initializers/sentry_javascript.rb b/config/initializers/sentry_javascript.rb index 584f39da..9fe3479c 100644 --- a/config/initializers/sentry_javascript.rb +++ b/config/initializers/sentry_javascript.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'sentry' + class SentryJavascript def self.active? dsn.present? && %w[development test].exclude?(environment) diff --git a/docs/LOCAL_SETUP.md b/docs/LOCAL_SETUP.md index db5fdd36..2384fefe 100644 --- a/docs/LOCAL_SETUP.md +++ b/docs/LOCAL_SETUP.md @@ -194,7 +194,7 @@ source "$HOME/.profile" - Create all necessary config files: ```bash - for f in action_mailer.yml database.yml secrets.yml code_ocean.yml docker.yml.erb mnemosyne.yml + for f in action_mailer.yml database.yml secrets.yml code_ocean.yml docker.yml.erb mnemosyne.yml content_security_policy.yml do if [ ! -f config/$f ] then @@ -303,7 +303,7 @@ source "$HOME/.profile" ``` - Get a local copy of the config files and verify the settings: ```shell script - for f in action_mailer.yml database.yml secrets.yml code_ocean.yml docker.yml.erb mnemosyne.yml + for f in action_mailer.yml database.yml secrets.yml code_ocean.yml docker.yml.erb mnemosyne.yml content_security_policy.yml do if [ ! -f config/$f ] then diff --git a/provision/provision.vagrant.sh b/provision/provision.vagrant.sh index ecaf297b..6137d7f2 100644 --- a/provision/provision.vagrant.sh +++ b/provision/provision.vagrant.sh @@ -92,7 +92,7 @@ gem install bundler cd /home/vagrant/codeocean # config -for f in action_mailer.yml database.yml secrets.yml docker.yml.erb mnemosyne.yml +for f in action_mailer.yml database.yml secrets.yml docker.yml.erb mnemosyne.yml content_security_policy.yml do if [ ! -f config/$f ] then