diff --git a/Changelog.md b/Changelog.md index 920431f36..e344122b0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -50,6 +50,7 @@ Note: Although this is a minor release, the configuration file changed because t * Don't federate to pods that have been offline for an extended period of time [#7120](https://github.com/diaspora/diaspora/pull/7120) * Add In-Reply-To and References headers to notification mails [#7122](https://github.com/diaspora/diaspora/pull/7122) * Directly link to a comment in commented notification mails [#7124](https://github.com/diaspora/diaspora/pull/7124) +* Add optional `Content-Security-Policy` header [#7128](https://github.com/diaspora/diaspora/pull/7128) # 0.6.0.1 diff --git a/Gemfile b/Gemfile index 873178880..5a667751c 100644 --- a/Gemfile +++ b/Gemfile @@ -137,6 +137,10 @@ gem "twitter-text", "1.14.0" gem "ruby-oembed", "0.10.1" gem "open_graph_reader", "0.6.1" +# Security Headers + +gem "secure_headers", "3.4.1" + # Services gem "omniauth", "1.3.1" @@ -211,10 +215,6 @@ group :production do # we don"t install these on travis to speed up test runs gem "rack-google-analytics", "1.2.0" gem "rack-piwik", "0.3.0", require: "rack/piwik" - # Click-jacking protection - - gem "rack-protection", "1.5.3" - # Process management gem "eye", "0.8.1" diff --git a/Gemfile.lock b/Gemfile.lock index 038bb6c4f..d398a53f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -780,6 +780,8 @@ GEM scss_lint (0.49.0) rake (>= 0.9, < 12) sass (~> 3.4.20) + secure_headers (3.4.1) + useragent securecompare (1.0.0) shellany (0.0.1) shoulda-matchers (3.1.1) @@ -877,6 +879,7 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) url_safe_base64 (0.2.2) + useragent (0.16.8) uuid (2.3.8) macaddr (~> 1.0) valid (1.2.0) @@ -993,7 +996,6 @@ DEPENDENCIES rack-cors (= 0.4.0) rack-google-analytics (= 1.2.0) rack-piwik (= 0.3.0) - rack-protection (= 1.5.3) rack-rewrite (= 1.5.1) rack-ssl (= 1.4.1) rails (= 4.2.7.1) @@ -1026,6 +1028,7 @@ DEPENDENCIES ruby-oembed (= 0.10.1) rubyzip (= 1.2.0) sass-rails (= 5.0.6) + secure_headers (= 3.4.1) shoulda-matchers (= 3.1.1) sidekiq (= 4.1.4) sidekiq-cron (= 0.4.2) diff --git a/app/helpers/analytics_helper.rb b/app/helpers/analytics_helper.rb index 1a17d3c9c..df7f69c6e 100644 --- a/app/helpers/analytics_helper.rb +++ b/app/helpers/analytics_helper.rb @@ -5,7 +5,7 @@ module AnalyticsHelper def include_mixpanel include_analytics "mixpanel" do - javascript_tag do + nonced_javascript_tag do <<-JS.html_safe (function(d,c){var a,b,g,e;a=d.createElement('script');a.type='text/javascript';a.async=!0;a.src=('https:'===d.location.protocol?'https:':'http:')+'//api.mixpanel.com/site_media/js/api/mixpanel.2.js';b=d.getElementsByTagName('script')[0];b.parentNode.insertBefore(a,b);c._i=[];c.init=function(a,d,f){var b=c;'undefined'!==typeof f?b=c[f]=[]:f='mixpanel';g='disable track track_pageview track_links track_forms register register_once unregister identify name_tag set_config'.split(' '); for(e=0;e true) + = include_gon(camel_case: true, nonce: content_security_policy_nonce(:script)) %body #app = render "layouts/header" diff --git a/config.ru b/config.ru index 2f5ac99d8..44a140077 100644 --- a/config.ru +++ b/config.ru @@ -17,6 +17,5 @@ if defined?(Unicorn) end use Rack::Deflater use Rack::InternetExplorerVersion, minimum: 9 -use Rack::Protection::FrameOptions run Diaspora::Application diff --git a/config/defaults.yml b/config/defaults.yml index 3d44def06..d8bb15aed 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -148,6 +148,9 @@ defaults: default_metas: title: 'diaspora* social network' description: 'diaspora* is the online social world where you are in control.' + csp: + report_only: true + report_uri: services: facebook: enable: false diff --git a/config/diaspora.yml.example b/config/diaspora.yml.example index 830f6a15a..25f7291a2 100644 --- a/config/diaspora.yml.example +++ b/config/diaspora.yml.example @@ -551,6 +551,26 @@ configuration: ## Section #title: 'diaspora* social network' #description: 'diaspora* is the online social world where you are in control.' + ## CSP (Content Security Policy) header + ## CSP allows limiting origins from where resources are allowed to be loaded. This + ## improves security, since it helps to detect and mitigate cross-site scripting + ## and data injection attacks. The default policy of diaspora* allows all third + ## party domains from services that are included in diaspora*, like OEmbed + ## scripts, so you can safely activate it by setting `report_only` to false. If + ## you customized diaspora* (edited templates or added own JS), additional work + ## may be required. You can test the policy with the "report_uri". Our default CSP + ## does not work with Google analytics or Piwik, because they inject JS code that + ## is blocked by CSP. + csp: + ## Report-Only header (default=true) + ## By default diaspora* adds only a "Content-Security-Policy-Report-Only" header. If you set + ## this to false, the "Content-Security-Policy" header is added instead. + #report_only: false + + ## CSP report URI (default=) + ## You can set an URI here, where the user agent reports violations as JSON document via a POST request. + #report_uri: "/csp_violation_reports" + ## Posting from Diaspora to external services (all are disabled by default). services: ## Section diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb new file mode 100644 index 000000000..cbc3721d1 --- /dev/null +++ b/config/initializers/secure_headers.rb @@ -0,0 +1,59 @@ +SecureHeaders::Configuration.default do |config| + config.hsts = SecureHeaders::OPT_OUT # added by Rack::SSL + + config.csp = { + default_src: %w('none'), + child_src: %w('self' www.youtube.com w.soundcloud.com twitter.com platform.twitter.com syndication.twitter.com + player.vimeo.com www.mixcloud.com www.dailymotion.com media.ccc.de bandcamp.com + www.instagram.com), + connect_src: %w('self' embedr.flickr.com geo.query.yahoo.com nominatim.openstreetmap.org api.github.com), + font_src: %w('self'), + form_action: %w('self' platform.twitter.com syndication.twitter.com), + frame_ancestors: %w('self'), + img_src: %w('self' data: *), + media_src: %w(https:), + script_src: %w('self' 'unsafe-eval' platform.twitter.com cdn.syndication.twimg.com widgets.flickr.com + embedr.flickr.com platform.instagram.com 'unsafe-inline'), + style_src: %w('self' 'unsafe-inline' platform.twitter.com *.twimg.com) + } + + if AppConfig.environment.assets.host.present? + asset_host = Addressable::URI.parse(AppConfig.environment.assets.host.get).host + config.csp[:script_src] << asset_host + config.csp[:style_src] << asset_host + end + + if AppConfig.chat.enabled? + config.csp[:media_src] << "data:" + + unless AppConfig.chat.server.bosh.proxy? + config.csp[:connect_src] << "#{AppConfig.pod_uri.host}:#{AppConfig.chat.server.bosh.port}" + end + end + + if AppConfig.privacy.mixpanel_uid.present? + config.csp[:script_src] << "api.mixpanel.com" + config.csp[:connect_src] << "api.mixpanel.com" + end + + config.csp[:script_src] << "code.jquery.com" if AppConfig.privacy.jquery_cdn? + config.csp[:script_src] << "static.chartbeat.com" if AppConfig.privacy.chartbeat_uid.present? + config.csp[:form_action] << "www.paypal.com" if AppConfig.settings.paypal_donations.enable? + + config.csp[:report_only] = AppConfig.settings.csp.report_only? + config.csp[:report_uri] = [AppConfig.settings.csp.report_uri] if AppConfig.settings.csp.report_uri.present? + + # Add frame-src but don't spam the log with DEPRECATION warnings. + # We need frame-src to support older versions of Chrome, because secure_headers handles all Chrome browsers as + # "modern" browser, and ignores the version of the browser. We can drop this once we support only Chrome + # versions with child-src support. + module SecureHeaders + class ContentSecurityPolicy + private + + def normalize_child_frame_src + @config[:frame_src] = @config[:child_src] + end + end + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 092b8f6a1..4c3ec1023 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -60,11 +60,11 @@ describe ApplicationHelper, :type => :helper do end it 'inclues jquery.js from jquery cdn' do - expect(jquery_include_tag).to match(/jquery\.com/) + expect(helper.jquery_include_tag).to match(/jquery\.com/) end it 'falls back to asset pipeline on cdn failure' do - expect(jquery_include_tag).to match(/document\.write/) + expect(helper.jquery_include_tag).to match(/document\.write/) end end @@ -74,17 +74,17 @@ describe ApplicationHelper, :type => :helper do end it 'includes jquery.js from asset pipeline' do - expect(jquery_include_tag).to match(/jquery2\.js/) - expect(jquery_include_tag).not_to match(/jquery\.com/) + expect(helper.jquery_include_tag).to match(/jquery2\.js/) + expect(helper.jquery_include_tag).not_to match(/jquery\.com/) end end it 'inclues jquery_ujs.js' do - expect(jquery_include_tag).to match(/jquery_ujs\.js/) + expect(helper.jquery_include_tag).to match(/jquery_ujs\.js/) end it "disables ajax caching" do - expect(jquery_include_tag).to match(/jQuery\.ajaxSetup/) + expect(helper.jquery_include_tag).to match(/jQuery\.ajaxSetup/) end end