From 8b01d24eed2e56577959f128b510a821a8d80c8c Mon Sep 17 00:00:00 2001 From: Tom Daniels Date: Mon, 19 Jan 2015 11:21:12 -0800 Subject: [PATCH 001/636] add HPKP support --- README.md | 27 ++++++ lib/secure_headers.rb | 15 ++- lib/secure_headers/headers/public_key_pins.rb | 95 +++++++++++++++++++ .../headers/public_key_pins_spec.rb | 37 ++++++++ spec/lib/secure_headers_spec.rb | 49 +++++++++- spec/spec_helper.rb | 1 + 6 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 lib/secure_headers/headers/public_key_pins.rb create mode 100644 spec/lib/secure_headers/headers/public_key_pins_spec.rb diff --git a/README.md b/README.md index 79f66bdb..97401b32 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The gem will automatically apply several headers that are related to security. - X-Content-Type-Options - [Prevent content type sniffing](http://msdn.microsoft.com/en-us/library/ie/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) +- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorites. [Public Key Pinnning Specification](https://tools.ietf.org/html/draft-ietf-websec-key-pinning-21) ## Usage @@ -21,6 +22,7 @@ The following methods are going to be called, unless they are provided in a `ski * `:set_csp_header` * `:set_hsts_header` +* `:set_hpkp_header` * `:set_x_frame_options_header` * `:set_x_xss_protection_header` * `:set_x_content_type_options_header` @@ -51,6 +53,12 @@ This gem makes a few assumptions about how you will use some features. For exam :img_src => "https:", :report_uri => '//example.com/uri-directive' } + config.hpkp = { + :max_age => 60.days.to_i, + :include_subdomains => true, + :report_uri => '//example.com/uri-directive', + :pins => [{:sha256 => 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}] + } end # and then simply include this in application_controller.rb @@ -298,6 +306,24 @@ console.log("will raise an exception if not in script_hashes.yml!") <% end %> ``` +### Public Key Pins + +``` +config.hpkp = { + max_age: 60.days.to_i, # max_age is a required parameter + include_subdomains: true, # whether or not to apply pins to subdomains + # Per the spec, SHA256 hashes are the only currently supported format. + pins: [ + {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, + {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} + ], + enforce: true, # defaults to false (report-only mode) + report_uri: '//example.com/uri-directive', + app_name: 'example', + tag_report_uri: true +} +``` + ### Using with Sinatra Here's an example using SecureHeaders for Sinatra applications: @@ -321,6 +347,7 @@ require 'secure_headers' :img_src => "https: data:", :frame_src => "https: http:.twimg.com http://itunes.apple.com" } + config.hpkp = false end class Donkey < Sinatra::Application diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index f149c554..f50f611b 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -6,7 +6,7 @@ module Configuration class << self attr_accessor :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :csp, :x_download_options, :script_hashes, - :x_permitted_cross_domain_policies + :x_permitted_cross_domain_policies, :hpkp def configure &block instance_eval &block @@ -42,6 +42,7 @@ def ensure_security_headers options = {} self.secure_headers_options = options before_filter :prep_script_hash before_filter :set_hsts_header + before_filter :set_hpkp_header before_filter :set_x_frame_options_header before_filter :set_csp_header before_filter :set_x_xss_protection_header @@ -61,6 +62,7 @@ module InstanceMethods def set_security_headers(options = self.class.secure_headers_options) set_csp_header(request, options[:csp]) set_hsts_header(options[:hsts]) + set_hpkp_header(options[:hpkp]) set_x_frame_options_header(options[:x_frame_options]) set_x_xss_protection_header(options[:x_xss_protection]) set_x_content_type_options_header(options[:x_content_type_options]) @@ -136,6 +138,16 @@ def set_hsts_header(options=self.class.secure_headers_options[:hsts]) set_a_header(:hsts, StrictTransportSecurity, options) end + def set_hpkp_header(options=self.class.secure_headers_options[:hpkp]) + return unless request.ssl? + config = self.class.options_for :hpkp, options + + return if config == false || config.nil? + + hpkp_header = PublicKeyPins.new(config) + set_header(hpkp_header) + end + def set_x_download_options_header(options=self.class.secure_headers_options[:x_download_options]) set_a_header(:x_download_options, XDownloadOptions, options) end @@ -168,6 +180,7 @@ def set_header(name_or_header, value=nil) require "secure_headers/version" require "secure_headers/header" +require "secure_headers/headers/public_key_pins" require "secure_headers/headers/content_security_policy" require "secure_headers/headers/x_frame_options" require "secure_headers/headers/strict_transport_security" diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb new file mode 100644 index 00000000..9d20d42c --- /dev/null +++ b/lib/secure_headers/headers/public_key_pins.rb @@ -0,0 +1,95 @@ +module SecureHeaders + class PublicKeyPinsBuildError < StandardError; end + class PublicKeyPins < Header + module Constants + HPKP_HEADER_NAME = "Public-Key-Pins" + ENV_KEY = 'secure_headers.public_key_pins' + HASH_ALGORITHMS = [:sha256] + DIRECTIVES = [:max_age] + end + class << self + def symbol_to_hyphen_case sym + sym.to_s.gsub('_', '-') + end + end + include Constants + + def initialize(config=nil) + @config = validate_config(config) + + @pins = @config.fetch(:pins, nil) + @report_uri = @config.fetch(:report_uri, nil) + @app_name = @config.fetch(:app_name, nil) + @enforce = !!@config.fetch(:enforce, nil) + @include_subdomains = !!@config.fetch(:include_subdomains, nil) + @tag_report_uri = !!@config.fetch(:tag_report_uri, nil) + end + + def name + base = HPKP_HEADER_NAME + if !@enforce + base += "-Report-Only" + end + base + end + + def value + header_value = [ + generic_directives, + pin_directives, + report_uri_directive, + subdomain_directive + ].compact.join('; ').strip + end + + def validate_config(config) + raise PublicKeyPinsBuildError.new("config must be a hash.") unless config.is_a? Hash + + if !config[:max_age] + raise PublicKeyPinsBuildError.new("max-age is a required directive.") + elsif config[:max_age].to_s !~ /\A\d+\z/ + raise PublicKeyPinsBuildError.new("max-age must be a number. + #{config[:max_age]} was supplied.") + elsif config[:pins] and config[:pins].length < 2 + raise PublicKeyPinsBuildError.new("A minimum of 2 pins are required.") + end + + config + end + + def pin_directives + return nil if @pins.nil? + @pins.collect do |pin| + pin.map do |token, hash| + "pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token) + end + end.join('; ') + end + + def generic_directives + DIRECTIVES.collect do |directive_name| + build_directive(directive_name) if @config[directive_name] + end.join('; ') + end + + def build_directive(key) + "#{self.class.symbol_to_hyphen_case(key)}=#{@config[key]}" + end + + def report_uri_directive + return nil if @report_uri.nil? + + if @tag_report_uri + @report_uri = "#{@report_uri}?enforce=#{@enforce}" + @report_uri += "&app_name=#{@app_name}" if @app_name + end + + "report-uri=\"#{@report_uri}\"" + end + + + def subdomain_directive + @include_subdomains ? 'includeSubDomains' : nil + end + end +end diff --git a/spec/lib/secure_headers/headers/public_key_pins_spec.rb b/spec/lib/secure_headers/headers/public_key_pins_spec.rb new file mode 100644 index 00000000..6280a6b1 --- /dev/null +++ b/spec/lib/secure_headers/headers/public_key_pins_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +module SecureHeaders + describe PublicKeyPins do + specify{ expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins-Report-Only") } + specify{ expect(PublicKeyPins.new(max_age: 1234, enforce: true).name).to eq("Public-Key-Pins") } + + specify { expect(PublicKeyPins.new({max_age: 1234}).value).to eq("max-age=1234")} + specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234")} + specify { + config = {max_age: 1234, pins: [{sha256: 'base64encodedpin1'}, {sha256: 'base64encodedpin2'}]} + header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\"" + expect(PublicKeyPins.new(config).value).to eq(header_value) + } + + context "with an invalid configuration" do + it "raises an exception when max-age is not provided" do + expect { + PublicKeyPins.new(foo: 'bar') + }.to raise_error(PublicKeyPinsBuildError) + end + + it "raises an exception with an invalid max-age" do + expect { + PublicKeyPins.new(max_age: 'abc123') + }.to raise_error(PublicKeyPinsBuildError) + end + + it 'raises an exception with less than 2 pins' do + expect { + config = {max_age: 1234, pins: [{sha256: 'base64encodedpin'}]} + PublicKeyPins.new(config) + }.to raise_error(PublicKeyPinsBuildError) + end + end + end +end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 9556d9e4..0650d410 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -18,7 +18,7 @@ class DummyClass allow(subject).to receive(:request).and_return(request) end - ALL_HEADERS = Hash[[:hsts, :csp, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_permitted_cross_domain_policies].map{|header| [header, false]}] + ALL_HEADERS = Hash[[:hpkp, :hsts, :csp, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_permitted_cross_domain_policies].map{|header| [header, false]}] def stub_user_agent val allow(request).to receive_message_chain(:env, :[]).and_return(val) @@ -30,6 +30,7 @@ def options_for header def reset_config ::SecureHeaders::Configuration.configure do |config| + config.hpkp = nil config.hsts = nil config.x_frame_options = nil config.x_content_type_options = nil @@ -42,6 +43,7 @@ def reset_config def set_security_headers(subject) subject.set_csp_header + subject.set_hpkp_header subject.set_hsts_header subject.set_x_frame_options_header subject.set_x_content_type_options_header @@ -71,6 +73,7 @@ def set_security_headers(subject) subject.set_csp_header subject.set_x_frame_options_header subject.set_hsts_header + subject.set_hpkp_header subject.set_x_xss_protection_header subject.set_x_content_type_options_header subject.set_x_download_options_header @@ -115,6 +118,17 @@ def set_security_headers(subject) subject.set_hsts_header({:include_subdomains => true}) end + it "does not set the HPKP header if disabled" do + should_not_assign_header(HPKP_HEADER_NAME) + subject.set_hpkp_header + end + + it "does not set the HPKP header if request is over HTTP" do + allow(subject).to receive_message_chain(:request, :ssl?).and_return(false) + should_not_assign_header(HPKP_HEADER_NAME) + subject.set_hpkp_header(max_age: 1234) + end + it "does not set the CSP header if disabled" do stub_user_agent(USER_AGENTS[:chrome]) should_not_assign_header(HEADER_NAME) @@ -136,6 +150,7 @@ def set_security_headers(subject) it "does not set any headers when disabled" do ::SecureHeaders::Configuration.configure do |config| config.hsts = false + config.hpkp = false config.x_frame_options = false config.x_content_type_options = false config.x_xss_protection = false @@ -196,6 +211,38 @@ def set_security_headers(subject) end end + describe "#set_public_key_pins" do + it "sets the Public-Key-Pins header" do + should_assign_header(HPKP_HEADER_NAME + "-Report-Only", "max-age=1234") + subject.set_hpkp_header(max_age: 1234) + end + + it "allows you to enforce public key pinning" do + should_assign_header(HPKP_HEADER_NAME, "max-age=1234") + subject.set_hpkp_header(max_age: 1234, enforce: true) + end + + it "allows you to specific a custom max-age value" do + should_assign_header(HPKP_HEADER_NAME + "-Report-Only", 'max-age=1234') + subject.set_hpkp_header(max_age: 1234) + end + + it "allows you to specify includeSubdomains" do + should_assign_header(HPKP_HEADER_NAME, "max-age=1234; includeSubDomains") + subject.set_hpkp_header(max_age: 1234, include_subdomains: true, enforce: true) + end + + it "allows you to specify a report-uri" do + should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com\"") + subject.set_hpkp_header(max_age: 1234, report_uri: "https://foobar.com", enforce: true) + end + + it "allows you to specify a report-uri with app_name" do + should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com?enforce=true&app_name=my_app\"") + subject.set_hpkp_header(max_age: 1234, report_uri: "https://foobar.com", app_name: "my_app", tag_report_uri: true, enforce: true) + end + end + describe "#set_x_xss_protection" do it "sets the X-XSS-Protection header" do should_assign_header(X_XSS_PROTECTION_HEADER_NAME, SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 11924efe..4bb5ef74 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,6 +7,7 @@ Coveralls.wear! end +include ::SecureHeaders::PublicKeyPins::Constants include ::SecureHeaders::StrictTransportSecurity::Constants include ::SecureHeaders::ContentSecurityPolicy::Constants include ::SecureHeaders::XFrameOptions::Constants From 46307c7c5b1aef5dd91b7b916401dc2208676852 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 30 Apr 2015 12:51:16 -0700 Subject: [PATCH 002/636] remove some dead code --- spec/lib/secure_headers/view_helper_spec.rb | 27 +++++++++++++++++++++ spec/lib/secure_headers_spec.rb | 6 ----- 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 spec/lib/secure_headers/view_helper_spec.rb diff --git a/spec/lib/secure_headers/view_helper_spec.rb b/spec/lib/secure_headers/view_helper_spec.rb new file mode 100644 index 00000000..896d2436 --- /dev/null +++ b/spec/lib/secure_headers/view_helper_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require 'pry' +require 'activesupport' +module SecureHeaders + describe ViewHelpers do + include ViewHelpers + include ActionView::Helpers::CaptureHelper + include ActionView::Helpers::JavaScriptHelper + + describe "#nonced_javascript_tag" do + + it 'works with a block' do + binding.pry + nonced_javascript_tag("alert(1)") + end + + it 'works without a block' + + context 'with extra options' do + it 'works with a block' + + it 'works without a block' + + end + end + end +end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 9556d9e4..2c7a3614 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -18,16 +18,10 @@ class DummyClass allow(subject).to receive(:request).and_return(request) end - ALL_HEADERS = Hash[[:hsts, :csp, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_permitted_cross_domain_policies].map{|header| [header, false]}] - def stub_user_agent val allow(request).to receive_message_chain(:env, :[]).and_return(val) end - def options_for header - ALL_HEADERS.reject{|k,v| k == header} - end - def reset_config ::SecureHeaders::Configuration.configure do |config| config.hsts = nil From 8d2ae5b2ae78f72c6180bd398ee80ad6d4f9eb73 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 30 Apr 2015 12:55:37 -0700 Subject: [PATCH 003/636] oops. delete test that was accidentally added --- spec/lib/secure_headers/view_helper_spec.rb | 27 --------------------- 1 file changed, 27 deletions(-) delete mode 100644 spec/lib/secure_headers/view_helper_spec.rb diff --git a/spec/lib/secure_headers/view_helper_spec.rb b/spec/lib/secure_headers/view_helper_spec.rb deleted file mode 100644 index 896d2436..00000000 --- a/spec/lib/secure_headers/view_helper_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'spec_helper' -require 'pry' -require 'activesupport' -module SecureHeaders - describe ViewHelpers do - include ViewHelpers - include ActionView::Helpers::CaptureHelper - include ActionView::Helpers::JavaScriptHelper - - describe "#nonced_javascript_tag" do - - it 'works with a block' do - binding.pry - nonced_javascript_tag("alert(1)") - end - - it 'works without a block' - - context 'with extra options' do - it 'works with a block' - - it 'works without a block' - - end - end - end -end From f5dcfb81745d5deaba713238607efef940d3f1cb Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 30 Apr 2015 12:58:04 -0700 Subject: [PATCH 004/636] Add :report_uri to list of directives - closes #141 --- lib/secure_headers/headers/content_security_policy.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 49e953bf..55204344 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -31,7 +31,13 @@ module Constants :reflected_xss ] - ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER = [ + :report_uri + ] + + SOURCE_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + + ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER end include Constants @@ -102,7 +108,7 @@ def initialize(config=nil, options={}) @config = config.inject({}) do |hash, (key, value)| config_val = value.respond_to?(:call) ? value.call : value - if ALL_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings + if SOURCE_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings config_val = config_val.split if config_val.is_a? String if config_val.is_a?(Array) config_val = config_val.map do |val| From ab790f3b39b1c0c28ef119e99ca705d4d190cb58 Mon Sep 17 00:00:00 2001 From: Julich Mera Date: Thu, 30 Apr 2015 14:39:28 -0400 Subject: [PATCH 005/636] Update README.md Fix broken link. Update README.md, link to CSP level 2. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79f66bdb..760ecdb4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # SecureHeaders [![Build Status](https://travis-ci.org/twitter/secureheaders.png?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.png)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.png)](https://coveralls.io/r/twitter/secureheaders) The gem will automatically apply several headers that are related to security. This includes: -- Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 1.1 Specification](https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html) +- Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) - HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797) - X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options draft](https://tools.ietf.org/html/draft-ietf-websec-x-frame-options-02) - X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](http://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx) From 1ed91811013c9cd8d9cb0dd24b4af05050df5b8c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 4 May 2015 18:48:17 -0600 Subject: [PATCH 006/636] version bump --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 8c7ef18f..a3f3ff82 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.0.1" + VERSION = "2.0.2" end From e15fb198ea70c2dfc504dc1ca670c33b7f3df3fa Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 5 May 2015 17:22:45 -0600 Subject: [PATCH 007/636] hash syntax --- lib/secure_headers/headers/public_key_pins.rb | 2 +- spec/lib/secure_headers_spec.rb | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb index 9d20d42c..2bc2f714 100644 --- a/lib/secure_headers/headers/public_key_pins.rb +++ b/lib/secure_headers/headers/public_key_pins.rb @@ -50,7 +50,7 @@ def validate_config(config) elsif config[:max_age].to_s !~ /\A\d+\z/ raise PublicKeyPinsBuildError.new("max-age must be a number. #{config[:max_age]} was supplied.") - elsif config[:pins] and config[:pins].length < 2 + elsif config[:pins] && config[:pins].length < 2 raise PublicKeyPinsBuildError.new("A minimum of 2 pins are required.") end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index f61b9b6f..6f3d0532 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -120,7 +120,7 @@ def set_security_headers(subject) it "does not set the HPKP header if request is over HTTP" do allow(subject).to receive_message_chain(:request, :ssl?).and_return(false) should_not_assign_header(HPKP_HEADER_NAME) - subject.set_hpkp_header(max_age: 1234) + subject.set_hpkp_header(:max_age => 1234) end it "does not set the CSP header if disabled" do @@ -208,32 +208,32 @@ def set_security_headers(subject) describe "#set_public_key_pins" do it "sets the Public-Key-Pins header" do should_assign_header(HPKP_HEADER_NAME + "-Report-Only", "max-age=1234") - subject.set_hpkp_header(max_age: 1234) + subject.set_hpkp_header(:max_age => 1234) end it "allows you to enforce public key pinning" do should_assign_header(HPKP_HEADER_NAME, "max-age=1234") - subject.set_hpkp_header(max_age: 1234, enforce: true) + subject.set_hpkp_header(:max_age => 1234, :enforce => true) end it "allows you to specific a custom max-age value" do should_assign_header(HPKP_HEADER_NAME + "-Report-Only", 'max-age=1234') - subject.set_hpkp_header(max_age: 1234) + subject.set_hpkp_header(:max_age => 1234) end it "allows you to specify includeSubdomains" do should_assign_header(HPKP_HEADER_NAME, "max-age=1234; includeSubDomains") - subject.set_hpkp_header(max_age: 1234, include_subdomains: true, enforce: true) + subject.set_hpkp_header(:max_age => 1234, :include_subdomains => true, :enforce => true) end it "allows you to specify a report-uri" do should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com\"") - subject.set_hpkp_header(max_age: 1234, report_uri: "https://foobar.com", enforce: true) + subject.set_hpkp_header(:max_age => 1234, :report_uri => "https://foobar.com", :enforce => true) end it "allows you to specify a report-uri with app_name" do should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com?enforce=true&app_name=my_app\"") - subject.set_hpkp_header(max_age: 1234, report_uri: "https://foobar.com", app_name: "my_app", tag_report_uri: true, enforce: true) + subject.set_hpkp_header(:max_age => 1234, :report_uri => "https://foobar.com", :app_name => "my_app", :tag_report_uri => true, :enforce => true) end end From d4454c90fdcef6dd92649631a5c6e2a230520e37 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 6 May 2015 09:59:29 -0600 Subject: [PATCH 008/636] more hash rockets --- .ruby-version | 2 +- .../headers/public_key_pins_spec.rb | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.ruby-version b/.ruby-version index 02f2617f..399088bf 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-1.9.3-p484 +2.1.6 diff --git a/spec/lib/secure_headers/headers/public_key_pins_spec.rb b/spec/lib/secure_headers/headers/public_key_pins_spec.rb index 6280a6b1..4e90e085 100644 --- a/spec/lib/secure_headers/headers/public_key_pins_spec.rb +++ b/spec/lib/secure_headers/headers/public_key_pins_spec.rb @@ -2,13 +2,13 @@ module SecureHeaders describe PublicKeyPins do - specify{ expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins-Report-Only") } - specify{ expect(PublicKeyPins.new(max_age: 1234, enforce: true).name).to eq("Public-Key-Pins") } + specify{ expect(PublicKeyPins.new(:max_age => 1234).name).to eq("Public-Key-Pins-Report-Only") } + specify{ expect(PublicKeyPins.new(:max_age => 1234, :enforce => true).name).to eq("Public-Key-Pins") } - specify { expect(PublicKeyPins.new({max_age: 1234}).value).to eq("max-age=1234")} - specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234")} + specify { expect(PublicKeyPins.new({:max_age => 1234}).value).to eq("max-age=1234")} + specify { expect(PublicKeyPins.new(:max_age => 1234).value).to eq("max-age=1234")} specify { - config = {max_age: 1234, pins: [{sha256: 'base64encodedpin1'}, {sha256: 'base64encodedpin2'}]} + config = {:max_age => 1234, :pins => [{:sha256 => 'base64encodedpin1'}, {:sha256 => 'base64encodedpin2'}]} header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\"" expect(PublicKeyPins.new(config).value).to eq(header_value) } @@ -16,19 +16,19 @@ module SecureHeaders context "with an invalid configuration" do it "raises an exception when max-age is not provided" do expect { - PublicKeyPins.new(foo: 'bar') + PublicKeyPins.new(:foo => 'bar') }.to raise_error(PublicKeyPinsBuildError) end it "raises an exception with an invalid max-age" do expect { - PublicKeyPins.new(max_age: 'abc123') + PublicKeyPins.new(:max_age => 'abc123') }.to raise_error(PublicKeyPinsBuildError) end it 'raises an exception with less than 2 pins' do expect { - config = {max_age: 1234, pins: [{sha256: 'base64encodedpin'}]} + config = {:max_age => 1234, :pins => [{:sha256 => 'base64encodedpin'}]} PublicKeyPins.new(config) }.to raise_error(PublicKeyPinsBuildError) end From 8df5b97a3ad7394a8d0f48bb31075718f5a0d0b4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 6 May 2015 12:09:34 -0600 Subject: [PATCH 009/636] doc updates --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e6bb44bf..1e43c129 100644 --- a/README.md +++ b/README.md @@ -57,17 +57,20 @@ This gem makes a few assumptions about how you will use some features. For exam :max_age => 60.days.to_i, :include_subdomains => true, :report_uri => '//example.com/uri-directive', - :pins => [{:sha256 => 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}] + :pins => [ + {:sha256 => 'abc'}, + {:sha256 => '123'} + ] } end -# and then simply include this in application_controller.rb +# and then include this in application_controller.rb class ApplicationController < ActionController::Base ensure_security_headers end ``` -Or simply add it to application controller +Or do the config as a parameter to `ensure_security_headers` ```ruby ensure_security_headers( @@ -308,6 +311,8 @@ console.log("will raise an exception if not in script_hashes.yml!") ### Public Key Pins +Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work. + ``` config.hpkp = { max_age: 60.days.to_i, # max_age is a required parameter From b500b041bef3ca6571e55fbda2356161d6d30408 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 7 May 2015 12:32:51 -0600 Subject: [PATCH 010/636] version bump for hpkp support --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index a3f3ff82..1a9d0c07 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.0.2" + VERSION = "2.1.0" end From b9f6d6266667ca359b4bac5021660e751f3aa109 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 8 May 2015 09:35:30 -0600 Subject: [PATCH 011/636] update hpkp spec reference per @reedloden --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e43c129..04ff09e6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The gem will automatically apply several headers that are related to security. - X-Content-Type-Options - [Prevent content type sniffing](http://msdn.microsoft.com/en-us/library/ie/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) -- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorites. [Public Key Pinnning Specification](https://tools.ietf.org/html/draft-ietf-websec-key-pinning-21) +- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorites. [Public Key Pinnning Specification](https://tools.ietf.org/html/rfc7469) ## Usage From 75e9b03ef96463013e9fd34f61ebe7d8cec4ab9e Mon Sep 17 00:00:00 2001 From: Kolja Dummann Date: Wed, 13 May 2015 19:55:51 +0200 Subject: [PATCH 012/636] update NWebsec url Update NWebsec url to the new project page. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04ff09e6..05ef4138 100644 --- a/README.md +++ b/README.md @@ -418,7 +418,7 @@ end * Node.js (express) [helmet](https://github.com/evilpacket/helmet) and [hood](https://github.com/seanmonstar/hood) * J2EE Servlet >= 3.0 [highlines](https://github.com/sourceclear/headlines) -* ASP.NET - [NWebsec](http://nwebsec.codeplex.com/) +* ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) * Python - [django-csp](https://github.com/mozilla/django-csp/) + [commonware](https://github.com/jsocol/commonware/) * Go - [secureheader](https://github.com/kr/secureheader) From 1a9d2ea71a69a62aa1aa5b1694fe81fb29454d77 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Wed, 17 Jun 2015 11:13:14 -0400 Subject: [PATCH 013/636] Change `options_for` from class method to instance method so it can be used as a hook --- lib/secure_headers.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index f50f611b..e0e6142f 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -50,12 +50,6 @@ def ensure_security_headers options = {} before_filter :set_x_download_options_header before_filter :set_x_permitted_cross_domain_policies_header end - - # we can't use ||= because I'm overloading false => disable, nil => default - # both of which trigger the conditional assignment - def options_for(type, options) - options.nil? ? ::SecureHeaders::Configuration.send(type) : options - end end module InstanceMethods @@ -80,7 +74,7 @@ def set_csp_header(req = nil, config=nil) end config = self.class.secure_headers_options[:csp] if config.nil? - config = self.class.options_for :csp, config + config = secure_header_options_for :csp, config return if config == false @@ -140,7 +134,7 @@ def set_hsts_header(options=self.class.secure_headers_options[:hsts]) def set_hpkp_header(options=self.class.secure_headers_options[:hpkp]) return unless request.ssl? - config = self.class.options_for :hpkp, options + config = secure_header_options_for :hpkp, options return if config == false || config.nil? @@ -158,8 +152,15 @@ def set_x_permitted_cross_domain_policies_header(options=self.class.secure_heade private + # we can't use ||= because I'm overloading false => disable, nil => default + # both of which trigger the conditional assignment + def secure_header_options_for(type, options) + options.nil? ? ::SecureHeaders::Configuration.send(type) : options + end + + def set_a_header(name, klass, options=nil) - options = self.class.options_for name, options + options = secure_header_options_for name, options return if options == false header = klass.new(options) From f2fb777bc2c707b3e83edd1d1976a6bff9aec74c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 18 Jun 2015 12:26:25 -0700 Subject: [PATCH 014/636] Pass reference to controller to CSP callable config values e.g. ``` :enforce => lambda { |controller| controller.current_user.beta_testing? } ``` --- .../headers/content_security_policy.rb | 2 +- .../headers/content_security_policy_spec.rb | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 55204344..88214b15 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -106,7 +106,7 @@ def initialize(config=nil, options={}) # Config values can be string, array, or lamdba values @config = config.inject({}) do |hash, (key, value)| - config_val = value.respond_to?(:call) ? value.call : value + config_val = value.respond_to?(:call) ? value.call(@controller) : value if SOURCE_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings config_val = config_val.split if config_val.is_a? String diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 8b6af384..7ca0df6c 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -76,7 +76,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} end it "adds a @enforce and @app_name variables to the report uri" do - opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => lambda { 'twitter' }) + opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => proc { 'twitter' }) csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME)) expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter") end @@ -90,7 +90,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "accepts procs for report-uris" do opts = { :default_src => 'self', - :report_uri => lambda { "http://lambda/result" } + :report_uri => proc { "http://lambda/result" } } csp = ContentSecurityPolicy.new(opts) @@ -99,15 +99,29 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "accepts procs for other fields" do opts = { - :default_src => lambda { "http://lambda/result" }, - :enforce => lambda { true }, - :disable_fill_missing => lambda { true } + :default_src => proc { "http://lambda/result" }, + :enforce => proc { true }, + :disable_fill_missing => proc { true } } csp = ContentSecurityPolicy.new(opts) expect(csp.value).to eq("default-src http://lambda/result; img-src http://lambda/result data:;") expect(csp.name).to match("Content-Security-Policy") end + + it "passes a reference to the controller to the proc" do + controller = double + user = double(:beta_testing? => true) + + allow(controller).to receive(:current_user).and_return(user) + opts = { + :disable_fill_missing => true, + :default_src => "self", + :enforce => lambda { |c| c.current_user.beta_testing? } + } + csp = ContentSecurityPolicy.new(opts, :controller => controller) + expect(csp.name).to match("Content-Security-Policy") + end end end From abd16ca6ead3c196576498caaee9d32f8cbca2ba Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 18 Jun 2015 12:55:42 -0700 Subject: [PATCH 015/636] update docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 05ef4138..5ecb8739 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ This gem makes a few assumptions about how you will use some features. For exam config.x_permitted_cross_domain_policies = 'none' config.csp = { :default_src => "https: self", + :enforce => proc {|controller| contoller.current_user.enforce_csp? } :frame_src => "https: http:.twimg.com http://itunes.apple.com", :img_src => "https:", :report_uri => '//example.com/uri-directive' From 809e49947a1d0de8a02ac4dba626141857fdf2a8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 18 Jun 2015 12:56:00 -0700 Subject: [PATCH 016/636] version bump --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 1a9d0c07..c2d3e631 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.1.0" + VERSION = "2.2.0" end From 85d77155ac665962d073d482211a628717e5f882 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 18 Jun 2015 14:59:27 -0700 Subject: [PATCH 017/636] add post install message about semi-backwards incompatibility --- secure_headers.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index ca54834c..a6a28afe 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -19,4 +19,5 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_development_dependency "rake" + gem.post_install_message = "Warning: lambda config values will be broken until you add |controller|. e.g. :enforce => lambda { |controller| some_expression }" end From 9b77a7cbb9756cf3a3a9e38f298a0529580406df Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 24 Jun 2015 11:50:25 -0700 Subject: [PATCH 018/636] only use nonces for browsers that support it or fall back gracefully --- .../other_things_controller_spec.rb | 2 +- .../other_things_controller_spec.rb | 2 +- .../headers/content_security_policy.rb | 14 ++++++++-- secure_headers.gemspec | 1 + .../headers/content_security_policy_spec.rb | 27 +++++++++++++++++-- 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb b/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb index ebbf0f40..99a5b488 100644 --- a/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +++ b/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb @@ -13,7 +13,7 @@ def request(opts = {}) options = opts.merge( { 'HTTPS' => 'on', - 'HTTP_USER_AGENT' => "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0" + 'HTTP_USER_AGENT' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22" } ) diff --git a/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb b/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb index 10bcc8cd..6b602565 100644 --- a/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +++ b/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb @@ -13,7 +13,7 @@ def request(opts = {}) options = opts.merge( { 'HTTPS' => 'on', - 'HTTP_USER_AGENT' => "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0" + 'HTTP_USER_AGENT' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22" } ) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 88214b15..407c663b 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -1,6 +1,7 @@ require 'uri' require 'base64' require 'securerandom' +require 'user_agent_parser' module SecureHeaders class ContentSecurityPolicyBuildError < StandardError; end @@ -205,8 +206,12 @@ def translate_dir_value val elsif %{self none}.include?(val) "'#{val}'" elsif val == 'nonce' - self.class.set_nonce(@controller, nonce) - ["'nonce-#{nonce}'", "'unsafe-inline'"] + if supports_nonces?(@ua) + self.class.set_nonce(@controller, nonce) + ["'nonce-#{nonce}'", "'unsafe-inline'"] + else + "'unsafe-inline'" + end else val end @@ -258,5 +263,10 @@ def non_default_directives def build_directive(key) "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; " end + + def supports_nonces?(user_agent) + parsed_ua = UserAgentParser.parse(user_agent) + ["Chrome", "Opera", "Firefox"].include?(parsed_ua.family) + end end end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index a6a28afe..5353828f 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -19,5 +19,6 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_development_dependency "rake" + gem.add_dependency "user_agent_parser" gem.post_install_message = "Warning: lambda config values will be broken until you add |controller|. e.g. :enforce => lambda { |controller| some_expression }" end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 7ca0df6c..e4689962 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -17,7 +17,8 @@ module SecureHeaders FIREFOX_23 = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0" CHROME = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4" CHROME_25 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22" - + SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A" + OPERA = "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16" def request_for user_agent, request_uri=nil, options={:ssl => false} double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') ) @@ -184,11 +185,33 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} end context "when using a nonce" do - it "adds a nonce and unsafe-inline to the script-src value" do + it "adds a nonce and unsafe-inline to the script-src value when using chrome" do header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME)) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end + it "adds a nonce and unsafe-inline to the script-src value when using firefox" do + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(FIREFOX)) + expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") + end + + it "adds a nonce and unsafe-inline to the script-src value when using firefox" do + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(OPERA)) + expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") + end + + it "does not add a nonce and unsafe-inline to the script-src value when using Safari" do + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(SAFARI)) + expect(header.value).to include("script-src 'self' 'unsafe-inline'") + expect(header.value).not_to include("nonce") + end + + it "does not add a nonce and unsafe-inline to the script-src value when using IE" do + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(IE)) + expect(header.value).to include("script-src 'self' 'unsafe-inline'") + expect(header.value).not_to include("nonce") + end + it "adds a nonce and unsafe-inline to the style-src value" do header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME)) expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") From b8ca8180b6803bdc04825b64192c1672a6a1931b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 24 Jun 2015 11:53:07 -0700 Subject: [PATCH 019/636] fix copy/paste mistake --- spec/lib/secure_headers/headers/content_security_policy_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index e4689962..53a447c1 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -195,7 +195,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end - it "adds a nonce and unsafe-inline to the script-src value when using firefox" do + it "adds a nonce and unsafe-inline to the script-src value when using opera" do header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(OPERA)) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end From 53b6db4593eda964420307fea7cfff0f566a0d49 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 24 Jun 2015 13:58:12 -0700 Subject: [PATCH 020/636] version bump --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index c2d3e631..c5d39ef1 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.2.0" + VERSION = "2.2.1" end From 865ff8bfa16f5b6ab902fe0e8225f9b299363f2c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 25 Jun 2015 15:24:01 -0700 Subject: [PATCH 021/636] add tests for secure_headers_options_for --- .../app/controllers/other_things_controller.rb | 14 ++++++++++++++ .../controllers/other_things_controller_spec.rb | 8 +++++++- .../app/controllers/other_things_controller.rb | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb b/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb index 07dec4fb..d6db0161 100644 --- a/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +++ b/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb @@ -3,4 +3,18 @@ class OtherThingsController < ApplicationController def index end + + def other_action + render :text => 'yooooo' + end + + def secure_header_options_for(header, options) + if params[:action] == "other_action" + if header == :csp + options.merge(:style_src => 'self') + end + else + options + end + end end diff --git a/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb b/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb index 724268ec..ad6d2bf2 100644 --- a/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +++ b/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb @@ -12,11 +12,17 @@ expect(response.headers['X-Frame-Options']).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) end - it "sets the X-WebKit-CSP header" do + it "sets the CSP header" do get :index expect(response.headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; img-src 'self' data:;") end + it "sets per-action values based on secure_header_options_for" do + # munges :style_src => self into policy + get :other_action + expect(response.headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; img-src 'self' data:; style-src 'self';") + end + #mock ssl it "sets the Strict-Transport-Security header" do request.env['HTTPS'] = 'on' diff --git a/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb b/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb index 98eca8c2..20b6ec9e 100644 --- a/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +++ b/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb @@ -2,4 +2,4 @@ class OtherThingsController < ApplicationController def index end -end \ No newline at end of file +end From 032668c3eaeb72c1de81ce693648a4cef593a733 Mon Sep 17 00:00:00 2001 From: vanderhoorn Date: Fri, 26 Jun 2015 12:04:44 +0200 Subject: [PATCH 022/636] Fix missing comma in the recommended initializer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ecb8739..bdde044f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ This gem makes a few assumptions about how you will use some features. For exam config.x_permitted_cross_domain_policies = 'none' config.csp = { :default_src => "https: self", - :enforce => proc {|controller| contoller.current_user.enforce_csp? } + :enforce => proc {|controller| contoller.current_user.enforce_csp? }, :frame_src => "https: http:.twimg.com http://itunes.apple.com", :img_src => "https:", :report_uri => '//example.com/uri-directive' From 346bb4bdc4788b7a4f13ef7633fe4671a7a78738 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 2 Jul 2015 11:40:05 -0700 Subject: [PATCH 023/636] version bump --- lib/secure_headers/version.rb | 2 +- secure_headers.gemspec | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index c5d39ef1..6bb24901 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.2.1" + VERSION = "2.2.2" end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 5353828f..32c46b31 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -20,5 +20,4 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.add_development_dependency "rake" gem.add_dependency "user_agent_parser" - gem.post_install_message = "Warning: lambda config values will be broken until you add |controller|. e.g. :enforce => lambda { |controller| some_expression }" end From b0568284ed0652b39373a7d9b57997669018b9f1 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 2 Jul 2015 11:55:19 -0700 Subject: [PATCH 024/636] cover more ruby versions with coveralls, don't error out on 1.8.7 --- Gemfile | 2 +- spec/spec_helper.rb | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 1af61afa..5bcd8f37 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,6 @@ group :test do gem 'rspec', '>= 3.1' gem 'growl' gem 'rb-fsevent' - gem 'coveralls', :platforms => [:ruby_19] + gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21, :ruby_21] gem 'i18n', '< 0.7.0', :platforms => [:ruby_18] end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4bb5ef74..4f22d516 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,8 +3,11 @@ require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers') -if defined?(Coveralls) +begin + require 'coveralls' Coveralls.wear! +rescue LoadError + # damn you 1.8.7 end include ::SecureHeaders::PublicKeyPins::Constants From 547286d0b6653326a25c412df243bee65fcdc81e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 6 Jul 2015 11:06:43 -0700 Subject: [PATCH 025/636] Include coveralls for ruby 2.2 --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 5bcd8f37..716afd3f 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,6 @@ group :test do gem 'rspec', '>= 3.1' gem 'growl' gem 'rb-fsevent' - gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21, :ruby_21] + gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21, :ruby_21, :ruby_22] gem 'i18n', '< 0.7.0', :platforms => [:ruby_18] end From 8b0fc89d9aed71549b130ce2d0880ceff9407b6a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 6 Jul 2015 11:07:13 -0700 Subject: [PATCH 026/636] Build 2.2 on travis too --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 795c10cf..5ecf02aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: ruby rvm: + - "2.2" - "2.1" - "2.0.0" - "1.9.3" From 3ceed6ec9ff9047ab544cac3b1e9d9453506c63b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 6 Jul 2015 11:15:35 -0700 Subject: [PATCH 027/636] details, details... /cc @reedloden :smile: --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 716afd3f..07ed26bd 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,6 @@ group :test do gem 'rspec', '>= 3.1' gem 'growl' gem 'rb-fsevent' - gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21, :ruby_21, :ruby_22] + gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21, :ruby_22] gem 'i18n', '< 0.7.0', :platforms => [:ruby_18] end From 9a5ef3823e036920519f7eadbb56550f9a3e0749 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 6 Jul 2015 11:25:09 -0700 Subject: [PATCH 028/636] Revert "Build 2.2 on travis too" This reverts commit 8b0fc89d9aed71549b130ce2d0880ceff9407b6a. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5ecf02aa..795c10cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: ruby rvm: - - "2.2" - "2.1" - "2.0.0" - "1.9.3" From 4a0e9c25cb67b934c5650f7b1d18d1492d8a06b7 Mon Sep 17 00:00:00 2001 From: Reed Loden Date: Wed, 29 Jul 2015 02:41:50 -0700 Subject: [PATCH 029/636] Add blankie and django-security to similar libraries list --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bdde044f..55fe5019 100644 --- a/README.md +++ b/README.md @@ -418,9 +418,10 @@ end ## Similar libraries * Node.js (express) [helmet](https://github.com/evilpacket/helmet) and [hood](https://github.com/seanmonstar/hood) +* Node.js (hapi) [blankie](https://github.com/nlf/blankie) * J2EE Servlet >= 3.0 [highlines](https://github.com/sourceclear/headlines) * ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) -* Python - [django-csp](https://github.com/mozilla/django-csp/) + [commonware](https://github.com/jsocol/commonware/) +* Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) * Go - [secureheader](https://github.com/kr/secureheader) ## Authors From 29c370c0bd6165a283202dba87c8c7f2d12d9759 Mon Sep 17 00:00:00 2001 From: Blake Hitchcock Date: Fri, 14 Aug 2015 12:03:16 -0400 Subject: [PATCH 030/636] Allow user to disable auto data uri whitelist The current behavior results in a data URI being appended to the ```img-src``` directive regardless of whether or not the user actually wants to allow data URIs. This PR introduced a new configuration option that the user can specify in order to disable the auto-whitelisting. --- lib/secure_headers/headers/content_security_policy.rb | 6 ++++-- .../headers/content_security_policy_spec.rb | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 407c663b..3fe73eac 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -128,6 +128,7 @@ def initialize(config=nil, options={}) @disable_fill_missing = !!@config.delete(:disable_fill_missing) @enforce = !!@config.delete(:enforce) + @disable_img_src_data_uri = !!@config.delete(:disable_img_src_data_uri) @tag_report_uri = !!@config.delete(:tag_report_uri) @script_hashes = @config.delete(:script_hashes) || [] @@ -238,10 +239,11 @@ def report_uri_directive def generic_directives header_value = '' + data_uri = @disable_img_src_data_uri ? [] : ["data:"] if @config[:img_src] - @config[:img_src] = @config[:img_src] + ['data:'] unless @config[:img_src].include?('data:') + @config[:img_src] = @config[:img_src] + data_uri unless @config[:img_src].include?('data:') else - @config[:img_src] = @config[:default_src] + ['data:'] + @config[:img_src] = @config[:default_src] + data_uri end DIRECTIVES.each do |directive_name| diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 53a447c1..dcc337ca 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -156,6 +156,16 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") end + + it "doesn't add a duplicate data uri if img-src specifies it already" do + csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self data:', :disable_fill_missing => true}, :request => request_for(CHROME)) + expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") + end + + it "allows the user to disable img-src data: uris auto-whitelisting" do + csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_img_src_data_uri => true, :disable_fill_missing => true}, :request => request_for(CHROME)) + expect(csp.value).to eq("default-src 'self'; img-src 'self';") + end end it "fills in directives without values with default-src value" do From 4ec1de30f674047724b3ce89c257002f25c69577 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 14 Aug 2015 12:22:35 -0700 Subject: [PATCH 031/636] disable ruby 2.2 for now --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 07ed26bd..5818de24 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,6 @@ group :test do gem 'rspec', '>= 3.1' gem 'growl' gem 'rb-fsevent' - gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21, :ruby_22] + gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21] gem 'i18n', '< 0.7.0', :platforms => [:ruby_18] end From 5c202aecb0ed4a5d03eb7b3f03ba6340a1c40b9b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 14 Aug 2015 13:25:26 -0700 Subject: [PATCH 032/636] version bump --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 6bb24901..97a63c67 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.2.2" + VERSION = "2.2.3" end From 7c9362720e2ba2a660ef79070aa3637a3a6d8c16 Mon Sep 17 00:00:00 2001 From: Asankhaya Sharma Date: Mon, 17 Aug 2015 21:31:55 +0800 Subject: [PATCH 033/636] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55fe5019..d6eaaf4a 100644 --- a/README.md +++ b/README.md @@ -419,7 +419,7 @@ end * Node.js (express) [helmet](https://github.com/evilpacket/helmet) and [hood](https://github.com/seanmonstar/hood) * Node.js (hapi) [blankie](https://github.com/nlf/blankie) -* J2EE Servlet >= 3.0 [highlines](https://github.com/sourceclear/headlines) +* J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines) * ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) * Go - [secureheader](https://github.com/kr/secureheader) From 66c2b26db535f0bdc20ba5745ca2deb908bf3171 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 17 Aug 2015 14:43:57 -0700 Subject: [PATCH 034/636] add json export --- lib/secure_headers/headers/content_security_policy.rb | 6 ++++++ .../secure_headers/headers/content_security_policy_spec.rb | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 3fe73eac..65fe9229 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -2,6 +2,7 @@ require 'base64' require 'securerandom' require 'user_agent_parser' +require 'json' module SecureHeaders class ContentSecurityPolicyBuildError < StandardError; end @@ -166,6 +167,11 @@ def value end end + def to_json + build_value + @config.to_json + end + private def add_script_hashes diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index dcc337ca..d1d33c67 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -56,6 +56,12 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} end end + it "exports a policy to JSON" do + policy = ContentSecurityPolicy.new(default_opts) + expected = %({"default_src":["https:"],"script_src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style_src":["'unsafe-inline'","https:","about:"],"img_src":["https:","data:"]}) + expect(policy.to_json).to eq(expected) + end + context "when using hash sources" do it "adds hashes and unsafe-inline to the script-src" do policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123'])) From 8a677df4a5a9dbdb4bbf403ae26bf7e8a9fc2a7f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 17 Aug 2015 15:24:58 -0700 Subject: [PATCH 035/636] add ability to import a policy from json --- .../headers/content_security_policy.rb | 12 +++++++++++- .../headers/content_security_policy_spec.rb | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 65fe9229..b7604fdb 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -169,7 +169,17 @@ def value def to_json build_value - @config.to_json + @config.to_json.gsub(/(\w+)_src/, "\\1-src") + end + + def self.from_json(*json_configs) + json_configs.inject({}) do |combined_config, one_config| + one_config = one_config.gsub(/(\w+)-src/, "\\1_src") + config = JSON.parse(one_config, :symbolize_names => true) + combined_config.merge(config) do |_, lhs, rhs| + lhs | rhs + end + end end private diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index d1d33c67..39f0b5a9 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -58,7 +58,17 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "exports a policy to JSON" do policy = ContentSecurityPolicy.new(default_opts) - expected = %({"default_src":["https:"],"script_src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style_src":["'unsafe-inline'","https:","about:"],"img_src":["https:","data:"]}) + expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]}) + expect(policy.to_json).to eq(expected) + end + + it "imports JSON to build a policy" do + json1 = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"]}) + json2 = %({"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]}) + config = ContentSecurityPolicy.from_json(json1, json2) + policy = ContentSecurityPolicy.new(config.merge(:disable_fill_missing => true)) + + expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]}) expect(policy.to_json).to eq(expected) end From de5add4a9e0bb19dc81e81dd474f557b742e1f0a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 17 Aug 2015 15:30:47 -0700 Subject: [PATCH 036/636] disable 1.8.7 travis jobs (temporarily) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 795c10cf..8835a1e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ rvm: - "2.1" - "2.0.0" - "1.9.3" - - "1.8.7" - "jruby-19mode" sudo: false From 8a77f4783f739954e1e1c5aa4de9ae39a17c85df Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 17 Aug 2015 15:39:55 -0700 Subject: [PATCH 037/636] make test demonstrate combining policies --- .../secure_headers/headers/content_security_policy_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 39f0b5a9..fc9ff4b6 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -64,8 +64,9 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "imports JSON to build a policy" do json1 = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"]}) - json2 = %({"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]}) - config = ContentSecurityPolicy.from_json(json1, json2) + json2 = %({"style-src":["'unsafe-inline'"],"img-src":["https:","data:"]}) + json3 = %({"style-src":["https:","about:"]}) + config = ContentSecurityPolicy.from_json(json1, json2, json3) policy = ContentSecurityPolicy.new(config.merge(:disable_fill_missing => true)) expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]}) From 1e97eca746a9b2e1529d50e83f72cfafb4ad5b79 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Doyle Date: Mon, 24 Aug 2015 19:01:13 -0400 Subject: [PATCH 038/636] Fix typo contoller > controller --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6eaaf4a..daee656e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ This gem makes a few assumptions about how you will use some features. For exam config.x_permitted_cross_domain_policies = 'none' config.csp = { :default_src => "https: self", - :enforce => proc {|controller| contoller.current_user.enforce_csp? }, + :enforce => proc {|controller| controller.current_user.enforce_csp? }, :frame_src => "https: http:.twimg.com http://itunes.apple.com", :img_src => "https:", :report_uri => '//example.com/uri-directive' From 6632d6256e79663d1384d60fa752fe95fae1f654 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 26 Aug 2015 14:58:39 -0700 Subject: [PATCH 039/636] add post install message warning about dropping 1.8.7 support Fixes #154 --- lib/secure_headers/version.rb | 2 +- secure_headers.gemspec | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 97a63c67..767e57ce 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.2.3" + VERSION = "2.2.4" end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 32c46b31..0c4cde83 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -20,4 +20,5 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.add_development_dependency "rake" gem.add_dependency "user_agent_parser" + gem.post_install_message = "Heads up, secure_headers Ruby 1.8.7 support will go away in the next major release." end From 3bdcc4fe538bbdb828288f04d984b9affa0ee2be Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 26 Aug 2015 16:27:39 -0700 Subject: [PATCH 040/636] print deprecation warning at runtime instead of during gem installation --- lib/secure_headers.rb | 3 +++ secure_headers.gemspec | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index e0e6142f..10ad906c 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -39,6 +39,9 @@ def secure_headers_options end def ensure_security_headers options = {} + if RUBY_VERSION == "1.8.7" + warn "[DEPRECATION] secure_headers ruby 1.8.7 support will dropped in the next release" + end self.secure_headers_options = options before_filter :prep_script_hash before_filter :set_hsts_header diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 0c4cde83..32c46b31 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -20,5 +20,4 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.add_development_dependency "rake" gem.add_dependency "user_agent_parser" - gem.post_install_message = "Heads up, secure_headers Ruby 1.8.7 support will go away in the next major release." end From 140d29eaffb30a970162c50473d59ddff37a874d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 22 Sep 2015 13:29:46 -0700 Subject: [PATCH 041/636] Add rack-secure_headers reference --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index daee656e..91f71f22 100644 --- a/README.md +++ b/README.md @@ -417,6 +417,7 @@ end ## Similar libraries +* Rack [rack-secure_headers](https://github.com/harmoni/rack-secure_headers) * Node.js (express) [helmet](https://github.com/evilpacket/helmet) and [hood](https://github.com/seanmonstar/hood) * Node.js (hapi) [blankie](https://github.com/nlf/blankie) * J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines) From 4aee15ef85c7413aa6718a1ec9ea82f843eb6d03 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 23 Sep 2015 13:48:41 -0700 Subject: [PATCH 042/636] first pass at 'return all headers as a hash' --- lib/secure_headers.rb | 60 ++++++++++++------- .../headers/content_security_policy.rb | 2 + lib/secure_headers/headers/public_key_pins.rb | 1 + .../headers/strict_transport_security.rb | 1 + .../headers/x_content_type_options.rb | 3 +- .../headers/x_download_options.rb | 1 + lib/secure_headers/headers/x_frame_options.rb | 1 + .../x_permitted_cross_domain_policies.rb | 1 + .../headers/x_xss_protection.rb | 1 + 9 files changed, 49 insertions(+), 22 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 10ad906c..6958e87c 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -1,7 +1,32 @@ +require "secure_headers/version" +require "secure_headers/header" +require "secure_headers/headers/public_key_pins" +require "secure_headers/headers/content_security_policy" +require "secure_headers/headers/x_frame_options" +require "secure_headers/headers/strict_transport_security" +require "secure_headers/headers/x_xss_protection" +require "secure_headers/headers/x_content_type_options" +require "secure_headers/headers/x_download_options" +require "secure_headers/headers/x_permitted_cross_domain_policies" +require "secure_headers/railtie" +require "secure_headers/hash_helper" +require "secure_headers/view_helper" + module SecureHeaders SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml' HASHES_ENV_KEY = 'secure_headers.script_hashes' + ALL_HEADER_CLASSES = [ + SecureHeaders::ContentSecurityPolicy, + SecureHeaders::PublicKeyPins, + SecureHeaders::StrictTransportSecurity, + SecureHeaders::XContentTypeOptions, + SecureHeaders::XDownloadOptions, + SecureHeaders::XFrameOptions, + SecureHeaders::XPermittedCrossDomainPolicies, + SecureHeaders::XXssProtection + ] + module Configuration class << self attr_accessor :hsts, :x_frame_options, :x_content_type_options, @@ -24,6 +49,19 @@ def append_features(base) include InstanceMethods end end + + def header_hash(options = {}) + ALL_HEADER_CLASSES.inject({}) do |memo, klass| + header = get_a_header(klass::Constants::CONFIG_KEY, klass, ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY)) + memo[header.name] = header.value + memo + end + end + + def get_a_header(name, klass, options) + return if options == false + klass.new(options) + end end module ClassMethods @@ -161,13 +199,8 @@ def secure_header_options_for(type, options) options.nil? ? ::SecureHeaders::Configuration.send(type) : options end - def set_a_header(name, klass, options=nil) - options = secure_header_options_for name, options - return if options == false - - header = klass.new(options) - set_header(header) + set_header(self.class.get_a_header(name, klass, secure_header_options_for(name, options))) end def set_header(name_or_header, value=nil) @@ -180,18 +213,3 @@ def set_header(name_or_header, value=nil) end end end - - -require "secure_headers/version" -require "secure_headers/header" -require "secure_headers/headers/public_key_pins" -require "secure_headers/headers/content_security_policy" -require "secure_headers/headers/x_frame_options" -require "secure_headers/headers/strict_transport_security" -require "secure_headers/headers/x_xss_protection" -require "secure_headers/headers/x_content_type_options" -require "secure_headers/headers/x_download_options" -require "secure_headers/headers/x_permitted_cross_domain_policies" -require "secure_headers/railtie" -require "secure_headers/hash_helper" -require "secure_headers/view_helper" diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index b7604fdb..78794bf4 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -40,7 +40,9 @@ module Constants SOURCE_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER + CONFIG_KEY = :csp end + include Constants attr_reader :disable_fill_missing, :ssl_request diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb index 2bc2f714..1aa453b2 100644 --- a/lib/secure_headers/headers/public_key_pins.rb +++ b/lib/secure_headers/headers/public_key_pins.rb @@ -6,6 +6,7 @@ module Constants ENV_KEY = 'secure_headers.public_key_pins' HASH_ALGORITHMS = [:sha256] DIRECTIVES = [:max_age] + CONFIG_KEY = :hpkp end class << self def symbol_to_hyphen_case sym diff --git a/lib/secure_headers/headers/strict_transport_security.rb b/lib/secure_headers/headers/strict_transport_security.rb index 40deb573..b5114fcb 100644 --- a/lib/secure_headers/headers/strict_transport_security.rb +++ b/lib/secure_headers/headers/strict_transport_security.rb @@ -8,6 +8,7 @@ module Constants DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i MESSAGE = "The config value supplied for the HSTS header was invalid." + CONFIG_KEY = :hsts end include Constants diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index 54bbe888..e832d0d3 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -5,6 +5,7 @@ class XContentTypeOptions < Header module Constants X_CONTENT_TYPE_OPTIONS_HEADER_NAME = "X-Content-Type-Options" DEFAULT_VALUE = "nosniff" + CONFIG_KEY = :x_content_type_options end include Constants @@ -37,4 +38,4 @@ def validate_config end end end -end \ No newline at end of file +end diff --git a/lib/secure_headers/headers/x_download_options.rb b/lib/secure_headers/headers/x_download_options.rb index 79582f67..77d2401d 100644 --- a/lib/secure_headers/headers/x_download_options.rb +++ b/lib/secure_headers/headers/x_download_options.rb @@ -4,6 +4,7 @@ class XDownloadOptions < Header module Constants XDO_HEADER_NAME = "X-Download-Options" DEFAULT_VALUE = 'noopen' + CONFIG_KEY = :x_download_options end include Constants diff --git a/lib/secure_headers/headers/x_frame_options.rb b/lib/secure_headers/headers/x_frame_options.rb index c816fc2a..2014dba4 100644 --- a/lib/secure_headers/headers/x_frame_options.rb +++ b/lib/secure_headers/headers/x_frame_options.rb @@ -5,6 +5,7 @@ module Constants XFO_HEADER_NAME = "X-Frame-Options" DEFAULT_VALUE = 'SAMEORIGIN' VALID_XFO_HEADER = /\A(SAMEORIGIN\z|DENY\z|ALLOW-FROM[:\s])/i + CONFIG_KEY = :x_frame_options end include Constants diff --git a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb index f783897e..b92a62ef 100644 --- a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +++ b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb @@ -5,6 +5,7 @@ module Constants XPCDP_HEADER_NAME = "X-Permitted-Cross-Domain-Policies" DEFAULT_VALUE = 'none' VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) + CONFIG_KEY = :x_permitted_cross_domain_policies end include Constants diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index 7860c0f3..fee7c167 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -5,6 +5,7 @@ module Constants X_XSS_PROTECTION_HEADER_NAME = 'X-XSS-Protection' DEFAULT_VALUE = "1" VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i + CONFIG_KEY = :x_xss_protection end include Constants From c0983eac631e9614f092df91cf08dbdf4b74082e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 23 Sep 2015 14:02:34 -0700 Subject: [PATCH 043/636] fix obvious bug --- lib/secure_headers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 6958e87c..1853043b 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -200,7 +200,7 @@ def secure_header_options_for(type, options) end def set_a_header(name, klass, options=nil) - set_header(self.class.get_a_header(name, klass, secure_header_options_for(name, options))) + set_header(SecureHeaders::get_a_header(name, klass, secure_header_options_for(name, options))) end def set_header(name_or_header, value=nil) From 9ea825270ed1ecfac2e80ef49f9dd4087e316ac7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 23 Sep 2015 14:08:36 -0700 Subject: [PATCH 044/636] fix tests --- lib/secure_headers.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 1853043b..c50dc402 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -200,7 +200,9 @@ def secure_header_options_for(type, options) end def set_a_header(name, klass, options=nil) - set_header(SecureHeaders::get_a_header(name, klass, secure_header_options_for(name, options))) + options = secure_header_options_for(name, options) + return if options == false + set_header(SecureHeaders::get_a_header(name, klass, options)) end def set_header(name_or_header, value=nil) From 1fc66995e524c59e9bc5b8edab19c31816f32103 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 23 Sep 2015 14:28:03 -0700 Subject: [PATCH 045/636] add ability to override config and add tests --- lib/secure_headers.rb | 10 +++++++--- spec/lib/secure_headers_spec.rb | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index c50dc402..0c42a9df 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -18,7 +18,6 @@ module SecureHeaders ALL_HEADER_CLASSES = [ SecureHeaders::ContentSecurityPolicy, - SecureHeaders::PublicKeyPins, SecureHeaders::StrictTransportSecurity, SecureHeaders::XContentTypeOptions, SecureHeaders::XDownloadOptions, @@ -50,9 +49,14 @@ def append_features(base) end end - def header_hash(options = {}) + def header_hash(options = nil) ALL_HEADER_CLASSES.inject({}) do |memo, klass| - header = get_a_header(klass::Constants::CONFIG_KEY, klass, ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY)) + config = if options.is_a?(Hash) + options[klass::Constants::CONFIG_KEY] + else + ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY) + end + header = get_a_header(klass::Constants::CONFIG_KEY, klass, config) memo[header.name] = header.value memo end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 6f3d0532..765bde68 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -159,6 +159,24 @@ def set_security_headers(subject) end end + describe "SecureHeaders#header_hash" do + it "produces a hash of headers given a hash as config" do + hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true}) + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;") + end + + it "produces a hash of headers with default config" do + hash = SecureHeaders::header_hash + expect(hash['Content-Security-Policy-Report-Only']).to eq(SecureHeaders::ContentSecurityPolicy::Constants::DEFAULT_CSP_HEADER) + expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) + expect(hash[XDO_HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE) + expect(hash[HSTS_HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE) + expect(hash[X_XSS_PROTECTION_HEADER_NAME]).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE) + expect(hash[X_CONTENT_TYPE_OPTIONS_HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) + expect(hash[XPCDP_HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE) + end + end + describe "#set_x_frame_options_header" do it "sets the X-Frame-Options header" do should_assign_header(XFO_HEADER_NAME, SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) From 9b7083f872378772626cc0441e6f8e71742bfd76 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 23 Sep 2015 14:38:25 -0700 Subject: [PATCH 046/636] handle the case where only some config options are provided --- lib/secure_headers.rb | 3 ++- spec/lib/secure_headers_spec.rb | 32 ++++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 0c42a9df..7e16d2bb 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -51,11 +51,12 @@ def append_features(base) def header_hash(options = nil) ALL_HEADER_CLASSES.inject({}) do |memo, klass| - config = if options.is_a?(Hash) + config = if options.is_a?(Hash) && options[klass::Constants::CONFIG_KEY] options[klass::Constants::CONFIG_KEY] else ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY) end + header = get_a_header(klass::Constants::CONFIG_KEY, klass, config) memo[header.name] = header.value memo diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 765bde68..21c04f1f 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -160,20 +160,40 @@ def set_security_headers(subject) end describe "SecureHeaders#header_hash" do + def expect_default_values(hash) + expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) + expect(hash[XDO_HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE) + expect(hash[HSTS_HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE) + expect(hash[X_XSS_PROTECTION_HEADER_NAME]).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE) + expect(hash[X_CONTENT_TYPE_OPTIONS_HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) + expect(hash[XPCDP_HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE) + end + it "produces a hash of headers given a hash as config" do hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true}) expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;") + expect_default_values(hash) + end + + it "produces a hash with a mix of config values, override values, and default values" do + ::SecureHeaders::Configuration.configure do |config| + config.hsts = { :max_age => '123456'} + end + + hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true}) + ::SecureHeaders::Configuration.configure do |config| + config.hsts = nil + end + + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;") + expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) + expect(hash[HSTS_HEADER_NAME]).to eq("max-age=123456") end it "produces a hash of headers with default config" do hash = SecureHeaders::header_hash expect(hash['Content-Security-Policy-Report-Only']).to eq(SecureHeaders::ContentSecurityPolicy::Constants::DEFAULT_CSP_HEADER) - expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) - expect(hash[XDO_HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE) - expect(hash[HSTS_HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE) - expect(hash[X_XSS_PROTECTION_HEADER_NAME]).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE) - expect(hash[X_CONTENT_TYPE_OPTIONS_HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) - expect(hash[XPCDP_HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE) + expect_default_values(hash) end end From 1a47c6afbf99b024f533754ed5b3c78dda7b5d2f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 23 Sep 2015 14:41:27 -0700 Subject: [PATCH 047/636] documentation around behavior --- README.md | 29 +++++++++++++++++++++++++++++ lib/secure_headers.rb | 7 +++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 91f71f22..4394f2d9 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,35 @@ def before_load end ``` +### Using in rack middleware + +The `SecureHeaders::header_hash` generates a hash of all header values, which is useful for merging with rack middleware values. + +```ruby +class MySecureHeaders + include SecureHeaders + def initialize(app) + @app = app + end + + def call(env) + status, headers, response = @app.call(env) + security_headers = if override? + SecureHeaders::header_hash(:csp => false) # uses global config, but overrides CSP config + else + SecureHeaders::header_hash # uses global config + end + [status, headers.merge(security_headers), [response.body]] + end +end + +module Testapp + class Application < Rails::Application + config.middleware.use MySecureHeaders + end +end +``` + ## Similar libraries * Rack [rack-secure_headers](https://github.com/harmoni/rack-secure_headers) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 7e16d2bb..05cf1922 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -19,6 +19,7 @@ module SecureHeaders ALL_HEADER_CLASSES = [ SecureHeaders::ContentSecurityPolicy, SecureHeaders::StrictTransportSecurity, + SecureHeaders::PublicKeyPins, SecureHeaders::XContentTypeOptions, SecureHeaders::XDownloadOptions, SecureHeaders::XFrameOptions, @@ -57,8 +58,10 @@ def header_hash(options = nil) ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY) end - header = get_a_header(klass::Constants::CONFIG_KEY, klass, config) - memo[header.name] = header.value + unless klass == SecureHeaders::PublicKeyPins && !config.is_a?(Hash) + header = get_a_header(klass::Constants::CONFIG_KEY, klass, config) + memo[header.name] = header.value + end memo end end From 6ed1c419274272fa58606ceeb7b736bf42510817 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 23 Sep 2015 14:47:05 -0700 Subject: [PATCH 048/636] ensure that hpkp is set when provided --- spec/lib/secure_headers_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 21c04f1f..4b8d19f8 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -178,16 +178,28 @@ def expect_default_values(hash) it "produces a hash with a mix of config values, override values, and default values" do ::SecureHeaders::Configuration.configure do |config| config.hsts = { :max_age => '123456'} + config.hpkp = { + :enforce => true, + :max_age => 1000000, + :include_subdomains => true, + :report_uri => '//example.com/uri-directive', + :pins => [ + {:sha256 => 'abc'}, + {:sha256 => '123'} + ] + } end hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true}) ::SecureHeaders::Configuration.configure do |config| config.hsts = nil + config.hpkp = nil end expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;") expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) expect(hash[HSTS_HEADER_NAME]).to eq("max-age=123456") + expect(hash[HPKP_HEADER_NAME]).to eq(%{max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains}) end it "produces a hash of headers with default config" do From f0e64c498d25a38cdd977ca46c4fbcf40b999b35 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 30 Sep 2015 09:40:48 -1000 Subject: [PATCH 049/636] Bump minor version for new header_hash feature --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 767e57ce..db07b903 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.2.4" + VERSION = "2.3.0" end From 824defe10568f8e5d333f1bd1aa4b6ee38cb0834 Mon Sep 17 00:00:00 2001 From: orthographic-pedant Date: Wed, 30 Sep 2015 19:41:17 -0400 Subject: [PATCH 050/636] Fixed typographical error, changed authorites to authorities in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4394f2d9..c920ba9b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The gem will automatically apply several headers that are related to security. - X-Content-Type-Options - [Prevent content type sniffing](http://msdn.microsoft.com/en-us/library/ie/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) -- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorites. [Public Key Pinnning Specification](https://tools.ietf.org/html/rfc7469) +- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinnning Specification](https://tools.ietf.org/html/rfc7469) ## Usage From 97f6d36ef164328dbb044d2a3b0057d5a03ed144 Mon Sep 17 00:00:00 2001 From: Narsimham Chelluri Date: Thu, 1 Oct 2015 03:44:47 -0700 Subject: [PATCH 051/636] Bump Ruby version to 2.2.3 --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 399088bf..58594069 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.6 +2.2.3 From ceb748bd5de4c7665579d67057f18957509487a8 Mon Sep 17 00:00:00 2001 From: Narsimham Chelluri Date: Thu, 1 Oct 2015 03:46:26 -0700 Subject: [PATCH 052/636] Make error matchers more specific - fixes RSpec 3.3 warning: https://github.com/rspec/rspec-expectations/pull/768 --- spec/lib/secure_headers/headers/x_content_type_options_spec.rb | 2 +- spec/lib/secure_headers/headers/x_download_options_spec.rb | 2 +- .../headers/x_permitted_cross_domain_policies_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/secure_headers/headers/x_content_type_options_spec.rb b/spec/lib/secure_headers/headers/x_content_type_options_spec.rb index b1062e74..62f49f73 100644 --- a/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +++ b/spec/lib/secure_headers/headers/x_content_type_options_spec.rb @@ -27,7 +27,7 @@ module SecureHeaders it "doesn't accept anything besides no-sniff" do expect { XContentTypeOptions.new("donkey") - }.to raise_error + }.to raise_error(XContentTypeOptionsBuildError) end end end diff --git a/spec/lib/secure_headers/headers/x_download_options_spec.rb b/spec/lib/secure_headers/headers/x_download_options_spec.rb index 5e67bc05..f869afca 100644 --- a/spec/lib/secure_headers/headers/x_download_options_spec.rb +++ b/spec/lib/secure_headers/headers/x_download_options_spec.rb @@ -25,7 +25,7 @@ module SecureHeaders it "doesn't accept anything besides noopen" do expect { XDownloadOptions.new("open") - }.to raise_error + }.to raise_error(XDOBuildError) end end end diff --git a/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb b/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb index 74c6d35d..fb981b74 100644 --- a/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +++ b/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb @@ -57,7 +57,7 @@ module SecureHeaders it "doesn't accept invalid values" do expect { XPermittedCrossDomainPolicies.new("open") - }.to raise_error + }.to raise_error(XPCDPBuildError) end end end From 9f3f72c86e823c523d655d9ad69c6f6f52cec017 Mon Sep 17 00:00:00 2001 From: Narsimham Chelluri Date: Thu, 1 Oct 2015 04:22:42 -0700 Subject: [PATCH 053/636] Use Ruby 2.2 compatible nil ivar behavior - fix test calling `nil.instance_variable_set`; no change in non-test code - Fixes #155 --- .../headers/content_security_policy_spec.rb | 17 +++++++++-------- spec/lib/secure_headers_spec.rb | 4 ---- spec/spec_helper.rb | 4 ++++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index fc9ff4b6..3419eeb1 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -11,6 +11,7 @@ module SecureHeaders :style_src => "inline https: about:" } end + let(:controller) { DummyClass.new } IE = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)" FIREFOX = "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8" @@ -213,39 +214,39 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} context "when using a nonce" do it "adds a nonce and unsafe-inline to the script-src value when using chrome" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME)) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end it "adds a nonce and unsafe-inline to the script-src value when using firefox" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(FIREFOX)) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(FIREFOX), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end it "adds a nonce and unsafe-inline to the script-src value when using opera" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(OPERA)) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(OPERA), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end it "does not add a nonce and unsafe-inline to the script-src value when using Safari" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(SAFARI)) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(SAFARI), :controller => controller) expect(header.value).to include("script-src 'self' 'unsafe-inline'") expect(header.value).not_to include("nonce") end it "does not add a nonce and unsafe-inline to the script-src value when using IE" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(IE)) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(IE), :controller => controller) expect(header.value).to include("script-src 'self' 'unsafe-inline'") expect(header.value).not_to include("nonce") end it "adds a nonce and unsafe-inline to the style-src value" do - header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME)) + header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME), :controller => controller) expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end it "adds an identical nonce to the style and script-src directives" do - header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME)) + header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME), :controller => controller) nonce = header.nonce value = header.value expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'") @@ -253,7 +254,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} end it "does not add 'unsafe-inline' twice" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME)) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';") end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 4b8d19f8..1677be79 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -1,10 +1,6 @@ require 'spec_helper' describe SecureHeaders do - class DummyClass - include ::SecureHeaders - end - subject {DummyClass.new} let(:headers) {double} let(:response) {double(:headers => headers)} diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4f22d516..0e150518 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,6 +31,10 @@ :safari6 => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1" } +class DummyClass + include ::SecureHeaders +end + def should_assign_header name, value expect(response.headers).to receive(:[]=).with(name, value) end From 2aea33bda55c4c53fd8e596811f704f6a16427b3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 06:56:03 -1000 Subject: [PATCH 054/636] enable ruby 2.2 in travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 8835a1e8..6246523c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: ruby rvm: + - "2.2" - "2.1" - "2.0.0" - "1.9.3" From b7620aa30a0c0786024c491fa84dd2d6604b16e0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 07:15:23 -1000 Subject: [PATCH 055/636] update fixture apps to use rails 3.2.22 to support ruby 2.2 --- Gemfile | 3 ++- Rakefile | 4 ++-- fixtures/{rails_3_2_12 => rails_3_2_22}/.rspec | 0 fixtures/{rails_3_2_12_no_init => rails_3_2_22}/Gemfile | 3 ++- fixtures/{rails_3_2_12 => rails_3_2_22}/README.rdoc | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/Rakefile | 0 .../app/controllers/application_controller.rb | 0 .../app/controllers/other_things_controller.rb | 0 .../app/controllers/things_controller.rb | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/app/models/.gitkeep | 0 .../app/views/layouts/application.html.erb | 0 .../app/views/other_things/index.html.erb | 0 .../app/views/things/index.html.erb | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/config.ru | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/config/application.rb | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/config/boot.rb | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/config/environment.rb | 0 .../config/environments/test.rb | 0 .../config/initializers/secure_headers.rb | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/config/routes.rb | 0 .../{rails_3_2_12 => rails_3_2_22}/config/script_hashes.yml | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/lib/assets/.gitkeep | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/lib/tasks/.gitkeep | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/log/.gitkeep | 0 .../spec/controllers/other_things_controller_spec.rb | 0 .../spec/controllers/things_controller_spec.rb | 0 fixtures/{rails_3_2_12 => rails_3_2_22}/spec/spec_helper.rb | 0 .../vendor/assets/javascripts/.gitkeep | 0 .../vendor/assets/stylesheets/.gitkeep | 0 .../{rails_3_2_12 => rails_3_2_22}/vendor/plugins/.gitkeep | 0 .../{rails_3_2_12_no_init => rails_3_2_22_no_init}/.rspec | 0 fixtures/{rails_3_2_12 => rails_3_2_22_no_init}/Gemfile | 4 ++-- .../README.rdoc | 0 .../{rails_3_2_12_no_init => rails_3_2_22_no_init}/Rakefile | 0 .../app/controllers/application_controller.rb | 0 .../app/controllers/other_things_controller.rb | 0 .../app/controllers/things_controller.rb | 0 .../app/models/.gitkeep | 0 .../app/views/layouts/application.html.erb | 0 .../app/views/other_things/index.html.erb | 0 .../app/views/things/index.html.erb | 0 .../{rails_3_2_12_no_init => rails_3_2_22_no_init}/config.ru | 0 .../config/application.rb | 0 .../config/boot.rb | 0 .../config/environment.rb | 0 .../config/environments/test.rb | 0 .../config/routes.rb | 0 .../lib/assets/.gitkeep | 0 .../lib/tasks/.gitkeep | 0 .../log/.gitkeep | 0 .../spec/controllers/other_things_controller_spec.rb | 0 .../spec/controllers/things_controller_spec.rb | 0 .../spec/spec_helper.rb | 0 .../vendor/assets/javascripts/.gitkeep | 0 .../vendor/assets/stylesheets/.gitkeep | 0 .../vendor/plugins/.gitkeep | 0 56 files changed, 8 insertions(+), 6 deletions(-) rename fixtures/{rails_3_2_12 => rails_3_2_22}/.rspec (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22}/Gemfile (67%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/README.rdoc (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/Rakefile (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/app/controllers/application_controller.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/app/controllers/other_things_controller.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/app/controllers/things_controller.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/app/models/.gitkeep (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/app/views/layouts/application.html.erb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/app/views/other_things/index.html.erb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/app/views/things/index.html.erb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/config.ru (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/config/application.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/config/boot.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/config/environment.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/config/environments/test.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/config/initializers/secure_headers.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/config/routes.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/config/script_hashes.yml (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/lib/assets/.gitkeep (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/lib/tasks/.gitkeep (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/log/.gitkeep (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/spec/controllers/other_things_controller_spec.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/spec/controllers/things_controller_spec.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/spec/spec_helper.rb (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/vendor/assets/javascripts/.gitkeep (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/vendor/assets/stylesheets/.gitkeep (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22}/vendor/plugins/.gitkeep (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/.rspec (100%) rename fixtures/{rails_3_2_12 => rails_3_2_22_no_init}/Gemfile (72%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/README.rdoc (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/Rakefile (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/app/controllers/application_controller.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/app/controllers/other_things_controller.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/app/controllers/things_controller.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/app/models/.gitkeep (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/app/views/layouts/application.html.erb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/app/views/other_things/index.html.erb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/app/views/things/index.html.erb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/config.ru (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/config/application.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/config/boot.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/config/environment.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/config/environments/test.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/config/routes.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/lib/assets/.gitkeep (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/lib/tasks/.gitkeep (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/log/.gitkeep (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/spec/controllers/other_things_controller_spec.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/spec/controllers/things_controller_spec.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/spec/spec_helper.rb (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/vendor/assets/javascripts/.gitkeep (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/vendor/assets/stylesheets/.gitkeep (100%) rename fixtures/{rails_3_2_12_no_init => rails_3_2_22_no_init}/vendor/plugins/.gitkeep (100%) diff --git a/Gemfile b/Gemfile index 5818de24..93a2a150 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,8 @@ source 'https://rubygems.org' gemspec group :test do - gem 'rails', '3.2.12' + gem 'test-unit', '~> 3.0' + gem 'rails', '3.2.22' gem 'sqlite3', :platforms => [:ruby, :mswin, :mingw] gem 'jdbc-sqlite3', :platforms => [:jruby] gem 'rspec-rails', '>= 3.1' diff --git a/Rakefile b/Rakefile index ee96d268..581509a8 100644 --- a/Rakefile +++ b/Rakefile @@ -15,7 +15,7 @@ task :default => :all_spec desc "Run all specs, and test fixture apps" task :all_spec => :spec do pwd = Dir.pwd - Dir.chdir 'fixtures/rails_3_2_12' + Dir.chdir 'fixtures/rails_3_2_22' puts Dir.pwd str = `bundle install >> /dev/null; bundle exec rspec spec` puts str @@ -25,7 +25,7 @@ task :all_spec => :spec do end Dir.chdir pwd - Dir.chdir 'fixtures/rails_3_2_12_no_init' + Dir.chdir 'fixtures/rails_3_2_22_no_init' puts Dir.pwd puts `bundle install >> /dev/null; bundle exec rspec spec` diff --git a/fixtures/rails_3_2_12/.rspec b/fixtures/rails_3_2_22/.rspec similarity index 100% rename from fixtures/rails_3_2_12/.rspec rename to fixtures/rails_3_2_22/.rspec diff --git a/fixtures/rails_3_2_12_no_init/Gemfile b/fixtures/rails_3_2_22/Gemfile similarity index 67% rename from fixtures/rails_3_2_12_no_init/Gemfile rename to fixtures/rails_3_2_22/Gemfile index 6228d4b0..c674048a 100644 --- a/fixtures/rails_3_2_12_no_init/Gemfile +++ b/fixtures/rails_3_2_22/Gemfile @@ -1,5 +1,6 @@ source 'https://rubygems.org' -gem 'rails', '3.2.12' +gem 'test-unit', '~> 3.0' +gem 'rails', '3.2.22' gem 'rspec-rails', '>= 2.0.0' gem 'secure_headers', :path => '../..' diff --git a/fixtures/rails_3_2_12/README.rdoc b/fixtures/rails_3_2_22/README.rdoc similarity index 100% rename from fixtures/rails_3_2_12/README.rdoc rename to fixtures/rails_3_2_22/README.rdoc diff --git a/fixtures/rails_3_2_12/Rakefile b/fixtures/rails_3_2_22/Rakefile similarity index 100% rename from fixtures/rails_3_2_12/Rakefile rename to fixtures/rails_3_2_22/Rakefile diff --git a/fixtures/rails_3_2_12/app/controllers/application_controller.rb b/fixtures/rails_3_2_22/app/controllers/application_controller.rb similarity index 100% rename from fixtures/rails_3_2_12/app/controllers/application_controller.rb rename to fixtures/rails_3_2_22/app/controllers/application_controller.rb diff --git a/fixtures/rails_3_2_12/app/controllers/other_things_controller.rb b/fixtures/rails_3_2_22/app/controllers/other_things_controller.rb similarity index 100% rename from fixtures/rails_3_2_12/app/controllers/other_things_controller.rb rename to fixtures/rails_3_2_22/app/controllers/other_things_controller.rb diff --git a/fixtures/rails_3_2_12/app/controllers/things_controller.rb b/fixtures/rails_3_2_22/app/controllers/things_controller.rb similarity index 100% rename from fixtures/rails_3_2_12/app/controllers/things_controller.rb rename to fixtures/rails_3_2_22/app/controllers/things_controller.rb diff --git a/fixtures/rails_3_2_12/app/models/.gitkeep b/fixtures/rails_3_2_22/app/models/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12/app/models/.gitkeep rename to fixtures/rails_3_2_22/app/models/.gitkeep diff --git a/fixtures/rails_3_2_12/app/views/layouts/application.html.erb b/fixtures/rails_3_2_22/app/views/layouts/application.html.erb similarity index 100% rename from fixtures/rails_3_2_12/app/views/layouts/application.html.erb rename to fixtures/rails_3_2_22/app/views/layouts/application.html.erb diff --git a/fixtures/rails_3_2_12/app/views/other_things/index.html.erb b/fixtures/rails_3_2_22/app/views/other_things/index.html.erb similarity index 100% rename from fixtures/rails_3_2_12/app/views/other_things/index.html.erb rename to fixtures/rails_3_2_22/app/views/other_things/index.html.erb diff --git a/fixtures/rails_3_2_12/app/views/things/index.html.erb b/fixtures/rails_3_2_22/app/views/things/index.html.erb similarity index 100% rename from fixtures/rails_3_2_12/app/views/things/index.html.erb rename to fixtures/rails_3_2_22/app/views/things/index.html.erb diff --git a/fixtures/rails_3_2_12/config.ru b/fixtures/rails_3_2_22/config.ru similarity index 100% rename from fixtures/rails_3_2_12/config.ru rename to fixtures/rails_3_2_22/config.ru diff --git a/fixtures/rails_3_2_12/config/application.rb b/fixtures/rails_3_2_22/config/application.rb similarity index 100% rename from fixtures/rails_3_2_12/config/application.rb rename to fixtures/rails_3_2_22/config/application.rb diff --git a/fixtures/rails_3_2_12/config/boot.rb b/fixtures/rails_3_2_22/config/boot.rb similarity index 100% rename from fixtures/rails_3_2_12/config/boot.rb rename to fixtures/rails_3_2_22/config/boot.rb diff --git a/fixtures/rails_3_2_12/config/environment.rb b/fixtures/rails_3_2_22/config/environment.rb similarity index 100% rename from fixtures/rails_3_2_12/config/environment.rb rename to fixtures/rails_3_2_22/config/environment.rb diff --git a/fixtures/rails_3_2_12/config/environments/test.rb b/fixtures/rails_3_2_22/config/environments/test.rb similarity index 100% rename from fixtures/rails_3_2_12/config/environments/test.rb rename to fixtures/rails_3_2_22/config/environments/test.rb diff --git a/fixtures/rails_3_2_12/config/initializers/secure_headers.rb b/fixtures/rails_3_2_22/config/initializers/secure_headers.rb similarity index 100% rename from fixtures/rails_3_2_12/config/initializers/secure_headers.rb rename to fixtures/rails_3_2_22/config/initializers/secure_headers.rb diff --git a/fixtures/rails_3_2_12/config/routes.rb b/fixtures/rails_3_2_22/config/routes.rb similarity index 100% rename from fixtures/rails_3_2_12/config/routes.rb rename to fixtures/rails_3_2_22/config/routes.rb diff --git a/fixtures/rails_3_2_12/config/script_hashes.yml b/fixtures/rails_3_2_22/config/script_hashes.yml similarity index 100% rename from fixtures/rails_3_2_12/config/script_hashes.yml rename to fixtures/rails_3_2_22/config/script_hashes.yml diff --git a/fixtures/rails_3_2_12/lib/assets/.gitkeep b/fixtures/rails_3_2_22/lib/assets/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12/lib/assets/.gitkeep rename to fixtures/rails_3_2_22/lib/assets/.gitkeep diff --git a/fixtures/rails_3_2_12/lib/tasks/.gitkeep b/fixtures/rails_3_2_22/lib/tasks/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12/lib/tasks/.gitkeep rename to fixtures/rails_3_2_22/lib/tasks/.gitkeep diff --git a/fixtures/rails_3_2_12/log/.gitkeep b/fixtures/rails_3_2_22/log/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12/log/.gitkeep rename to fixtures/rails_3_2_22/log/.gitkeep diff --git a/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb b/fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb similarity index 100% rename from fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb rename to fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb diff --git a/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb b/fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb similarity index 100% rename from fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb rename to fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb diff --git a/fixtures/rails_3_2_12/spec/spec_helper.rb b/fixtures/rails_3_2_22/spec/spec_helper.rb similarity index 100% rename from fixtures/rails_3_2_12/spec/spec_helper.rb rename to fixtures/rails_3_2_22/spec/spec_helper.rb diff --git a/fixtures/rails_3_2_12/vendor/assets/javascripts/.gitkeep b/fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12/vendor/assets/javascripts/.gitkeep rename to fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep diff --git a/fixtures/rails_3_2_12/vendor/assets/stylesheets/.gitkeep b/fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12/vendor/assets/stylesheets/.gitkeep rename to fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep diff --git a/fixtures/rails_3_2_12/vendor/plugins/.gitkeep b/fixtures/rails_3_2_22/vendor/plugins/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12/vendor/plugins/.gitkeep rename to fixtures/rails_3_2_22/vendor/plugins/.gitkeep diff --git a/fixtures/rails_3_2_12_no_init/.rspec b/fixtures/rails_3_2_22_no_init/.rspec similarity index 100% rename from fixtures/rails_3_2_12_no_init/.rspec rename to fixtures/rails_3_2_22_no_init/.rspec diff --git a/fixtures/rails_3_2_12/Gemfile b/fixtures/rails_3_2_22_no_init/Gemfile similarity index 72% rename from fixtures/rails_3_2_12/Gemfile rename to fixtures/rails_3_2_22_no_init/Gemfile index f6f439dd..3cf56cf2 100644 --- a/fixtures/rails_3_2_12/Gemfile +++ b/fixtures/rails_3_2_22_no_init/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'rails', '3.2.12' +gem 'test-unit' +gem 'rails', '3.2.22' gem 'rspec-rails', '>= 2.0.0' gem 'secure_headers', :path => '../..' - diff --git a/fixtures/rails_3_2_12_no_init/README.rdoc b/fixtures/rails_3_2_22_no_init/README.rdoc similarity index 100% rename from fixtures/rails_3_2_12_no_init/README.rdoc rename to fixtures/rails_3_2_22_no_init/README.rdoc diff --git a/fixtures/rails_3_2_12_no_init/Rakefile b/fixtures/rails_3_2_22_no_init/Rakefile similarity index 100% rename from fixtures/rails_3_2_12_no_init/Rakefile rename to fixtures/rails_3_2_22_no_init/Rakefile diff --git a/fixtures/rails_3_2_12_no_init/app/controllers/application_controller.rb b/fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/app/controllers/application_controller.rb rename to fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb diff --git a/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb b/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb rename to fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb diff --git a/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb b/fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb rename to fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb diff --git a/fixtures/rails_3_2_12_no_init/app/models/.gitkeep b/fixtures/rails_3_2_22_no_init/app/models/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12_no_init/app/models/.gitkeep rename to fixtures/rails_3_2_22_no_init/app/models/.gitkeep diff --git a/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb b/fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb similarity index 100% rename from fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb rename to fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb diff --git a/fixtures/rails_3_2_12_no_init/app/views/other_things/index.html.erb b/fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb similarity index 100% rename from fixtures/rails_3_2_12_no_init/app/views/other_things/index.html.erb rename to fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb diff --git a/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb b/fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb similarity index 100% rename from fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb rename to fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb diff --git a/fixtures/rails_3_2_12_no_init/config.ru b/fixtures/rails_3_2_22_no_init/config.ru similarity index 100% rename from fixtures/rails_3_2_12_no_init/config.ru rename to fixtures/rails_3_2_22_no_init/config.ru diff --git a/fixtures/rails_3_2_12_no_init/config/application.rb b/fixtures/rails_3_2_22_no_init/config/application.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/config/application.rb rename to fixtures/rails_3_2_22_no_init/config/application.rb diff --git a/fixtures/rails_3_2_12_no_init/config/boot.rb b/fixtures/rails_3_2_22_no_init/config/boot.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/config/boot.rb rename to fixtures/rails_3_2_22_no_init/config/boot.rb diff --git a/fixtures/rails_3_2_12_no_init/config/environment.rb b/fixtures/rails_3_2_22_no_init/config/environment.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/config/environment.rb rename to fixtures/rails_3_2_22_no_init/config/environment.rb diff --git a/fixtures/rails_3_2_12_no_init/config/environments/test.rb b/fixtures/rails_3_2_22_no_init/config/environments/test.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/config/environments/test.rb rename to fixtures/rails_3_2_22_no_init/config/environments/test.rb diff --git a/fixtures/rails_3_2_12_no_init/config/routes.rb b/fixtures/rails_3_2_22_no_init/config/routes.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/config/routes.rb rename to fixtures/rails_3_2_22_no_init/config/routes.rb diff --git a/fixtures/rails_3_2_12_no_init/lib/assets/.gitkeep b/fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12_no_init/lib/assets/.gitkeep rename to fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep diff --git a/fixtures/rails_3_2_12_no_init/lib/tasks/.gitkeep b/fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12_no_init/lib/tasks/.gitkeep rename to fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep diff --git a/fixtures/rails_3_2_12_no_init/log/.gitkeep b/fixtures/rails_3_2_22_no_init/log/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12_no_init/log/.gitkeep rename to fixtures/rails_3_2_22_no_init/log/.gitkeep diff --git a/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb b/fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb rename to fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb diff --git a/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb b/fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb rename to fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb diff --git a/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb b/fixtures/rails_3_2_22_no_init/spec/spec_helper.rb similarity index 100% rename from fixtures/rails_3_2_12_no_init/spec/spec_helper.rb rename to fixtures/rails_3_2_22_no_init/spec/spec_helper.rb diff --git a/fixtures/rails_3_2_12_no_init/vendor/assets/javascripts/.gitkeep b/fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12_no_init/vendor/assets/javascripts/.gitkeep rename to fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep diff --git a/fixtures/rails_3_2_12_no_init/vendor/assets/stylesheets/.gitkeep b/fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12_no_init/vendor/assets/stylesheets/.gitkeep rename to fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep diff --git a/fixtures/rails_3_2_12_no_init/vendor/plugins/.gitkeep b/fixtures/rails_3_2_22_no_init/vendor/plugins/.gitkeep similarity index 100% rename from fixtures/rails_3_2_12_no_init/vendor/plugins/.gitkeep rename to fixtures/rails_3_2_22_no_init/vendor/plugins/.gitkeep From d9e96fcc0d68806adaaf90013ab84fcada04b79e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 07:31:17 -1000 Subject: [PATCH 056/636] Remove the 'fill missing directives with default-src value' feature --- README.md | 6 ------ .../config/initializers/secure_headers.rb | 1 - .../controllers/other_things_controller.rb | 2 +- .../config/initializers/secure_headers.rb | 1 - .../headers/content_security_policy.rb | 16 +------------- .../headers/content_security_policy_spec.rb | 21 +++++-------------- 6 files changed, 7 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index c920ba9b..8472267d 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,6 @@ The following methods are going to be called, unless they are provided in a `ski * `:set_x_download_options_header` * `:set_x_permitted_cross_domain_policies_header` -### Bonus Features - -This gem makes a few assumptions about how you will use some features. For example: - -* It fills any blank directives with the value in `:default_src` Getting a default\-src report is pretty useless. This way, you will always know what type of violation occurred. You can disable this feature by supplying `:disable_fill_missing => true`. This is referred to as the "effective-directive" in the spec, but is not well supported as of Nov 5, 2013. - ## Configuration **Place the following in an initializer (recommended):** diff --git a/fixtures/rails_3_2_22/config/initializers/secure_headers.rb b/fixtures/rails_3_2_22/config/initializers/secure_headers.rb index 3580a41d..9320dbf0 100644 --- a/fixtures/rails_3_2_22/config/initializers/secure_headers.rb +++ b/fixtures/rails_3_2_22/config/initializers/secure_headers.rb @@ -7,7 +7,6 @@ csp = { :default_src => "self", :script_src => "self nonce", - :disable_fill_missing => true, :report_uri => 'somewhere', :script_hash_middleware => true, :enforce => false # false means warnings only diff --git a/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb b/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb index d6db0161..72211331 100644 --- a/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb +++ b/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb @@ -1,5 +1,5 @@ class OtherThingsController < ApplicationController - ensure_security_headers :csp => {:default_src => 'self', :disable_fill_missing => true} + ensure_security_headers :csp => {:default_src => 'self'} def index end diff --git a/fixtures/rails_4_1_8/config/initializers/secure_headers.rb b/fixtures/rails_4_1_8/config/initializers/secure_headers.rb index 87f537fa..ea3eb847 100644 --- a/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +++ b/fixtures/rails_4_1_8/config/initializers/secure_headers.rb @@ -7,7 +7,6 @@ csp = { :default_src => "self", :script_src => "self nonce", - :disable_fill_missing => true, :report_uri => 'somewhere', :script_hash_middleware => true, :enforce => false # false means warnings only diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 78794bf4..b870b93a 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -45,8 +45,7 @@ module Constants include Constants - attr_reader :disable_fill_missing, :ssl_request - alias :disable_fill_missing? :disable_fill_missing + attr_reader :ssl_request alias :ssl_request? :ssl_request class << self @@ -128,15 +127,12 @@ def initialize(config=nil, options={}) @http_additions = @config.delete(:http_additions) @app_name = @config.delete(:app_name) @report_uri = @config.delete(:report_uri) - - @disable_fill_missing = !!@config.delete(:disable_fill_missing) @enforce = !!@config.delete(:enforce) @disable_img_src_data_uri = !!@config.delete(:disable_img_src_data_uri) @tag_report_uri = !!@config.delete(:tag_report_uri) @script_hashes = @config.delete(:script_hashes) || [] add_script_hashes if @script_hashes.any? - fill_directives unless disable_fill_missing? end ## @@ -200,16 +196,6 @@ def build_value ].join.strip end - def fill_directives - if default = @config[:default_src] - DIRECTIVES.each do |directive| - unless @config[directive] - @config[directive] = default - end - end - end - end - def append_http_additions return unless @http_additions @http_additions.each do |k, v| diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 3419eeb1..18660458 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -4,7 +4,6 @@ module SecureHeaders describe ContentSecurityPolicy do let(:default_opts) do { - :disable_fill_missing => true, :default_src => 'https:', :report_uri => '/csp_report', :script_src => 'inline eval https: data:', @@ -68,7 +67,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} json2 = %({"style-src":["'unsafe-inline'"],"img-src":["https:","data:"]}) json3 = %({"style-src":["https:","about:"]}) config = ContentSecurityPolicy.from_json(json1, json2, json3) - policy = ContentSecurityPolicy.new(config.merge(:disable_fill_missing => true)) + policy = ContentSecurityPolicy.new(config) expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]}) expect(policy.to_json).to eq(expected) @@ -83,7 +82,6 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} describe "#normalize_csp_options" do before(:each) do - default_opts.delete(:disable_fill_missing) default_opts[:script_src] << ' self none' @opts = default_opts end @@ -120,7 +118,6 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} opts = { :default_src => proc { "http://lambda/result" }, :enforce => proc { true }, - :disable_fill_missing => proc { true } } csp = ContentSecurityPolicy.new(opts) @@ -134,7 +131,6 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} allow(controller).to receive(:current_user).and_return(user) opts = { - :disable_fill_missing => true, :default_src => "self", :enforce => lambda { |c| c.current_user.beta_testing? } } @@ -166,33 +162,26 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} context "auto-whitelists data: uris for img-src" do it "sets the value if no img-src specified" do - csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME)) + csp = ContentSecurityPolicy.new({:default_src => 'self'}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") end it "appends the value if img-src is specified" do - csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME)) + csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self'}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") end it "doesn't add a duplicate data uri if img-src specifies it already" do - csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self data:', :disable_fill_missing => true}, :request => request_for(CHROME)) + csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self data:'}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") end it "allows the user to disable img-src data: uris auto-whitelisting" do - csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_img_src_data_uri => true, :disable_fill_missing => true}, :request => request_for(CHROME)) + csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_img_src_data_uri => true}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self';") end end - it "fills in directives without values with default-src value" do - options = default_opts.merge(:disable_fill_missing => false) - csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME)) - value = "default-src https:; connect-src https:; font-src https:; frame-src https:; img-src https: data:; media-src https:; object-src https:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;" - expect(csp.value).to eq(value) - end - it "sends the standard csp header if an unknown browser is supplied" do csp = ContentSecurityPolicy.new(default_opts, :request => request_for(IE)) expect(csp.value).to match "default-src" From fc83d4f99d048b664214e51d8a0bebeb7c769e26 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 07:40:05 -1000 Subject: [PATCH 057/636] remove examples that use inline/eval over 'unsafe-inline'/'unsafe-eval' --- README.md | 2 +- .../content_security_policy/script_hash_middleware_spec.rb | 4 ++-- .../secure_headers/headers/content_security_policy_spec.rb | 6 +++--- spec/lib/secure_headers_spec.rb | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c920ba9b..3bab4bb3 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ This configuration will likely work for most applications without modification. ```ruby # most basic example :csp => { - :default_src => "https: inline eval", + :default_src => "https: 'unsafe-inline' 'unsafe-eval'", :report_uri => '/uri-directive' } diff --git a/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb b/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb index 10259fe0..62dbbd19 100644 --- a/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb @@ -13,8 +13,8 @@ module SecureHeaders :disable_fill_missing => true, :default_src => 'https://*', :report_uri => '/csp_report', - :script_src => 'inline eval https://* data:', - :style_src => "inline https://* about:" + :script_src => "'unsafe-inline' 'unsafe-eval' https://* data:", + :style_src => "'unsafe-inline' https://* about:" } end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 3419eeb1..e4878553 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -7,8 +7,8 @@ module SecureHeaders :disable_fill_missing => true, :default_src => 'https:', :report_uri => '/csp_report', - :script_src => 'inline eval https: data:', - :style_src => "inline https: about:" + :script_src => "'unsafe-inline' 'unsafe-eval' https: data:", + :style_src => "'unsafe-inline' https: about:" } end let(:controller) { DummyClass.new } @@ -254,7 +254,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} end it "does not add 'unsafe-inline' twice" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce 'unsafe-inline'"), :request => request_for(CHROME), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';") end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 1677be79..dbf9ac28 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -127,7 +127,7 @@ def set_security_headers(subject) it "saves the options to the env when using script hashes" do opts = { - :default_src => 'self', + :default_src => "'self'", :script_hash_middleware => true } stub_user_agent(USER_AGENTS[:chrome]) @@ -166,7 +166,7 @@ def expect_default_values(hash) end it "produces a hash of headers given a hash as config" do - hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true}) + hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:", :disable_fill_missing => true}) expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;") expect_default_values(hash) end @@ -186,7 +186,7 @@ def expect_default_values(hash) } end - hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true}) + hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:", :disable_fill_missing => true}) ::SecureHeaders::Configuration.configure do |config| config.hsts = nil config.hpkp = nil From ddf75e81e1c9b749092e16d4cf6f2a75c362acdf Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 07:42:08 -1000 Subject: [PATCH 058/636] Add deprecation warnings --- lib/secure_headers/headers/content_security_policy.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 78794bf4..2c069ca4 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -220,9 +220,10 @@ def append_http_additions def translate_dir_value val if %w{inline eval}.include?(val) + warn "[DEPRECATION] using inline/eval may not be supported in the future. Instead use 'unsafe-inline'/'unsafe-eval' instead." val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'" - # self/none are special sources/src-dir-values and need to be quoted elsif %{self none}.include?(val) + warn "[DEPRECATION] using self/none may not be supported in the future. Instead use 'self'/'none' instead." "'#{val}'" elsif val == 'nonce' if supports_nonces?(@ua) From b4b219222cc4c5b58876cc02c5ef690312f3d067 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 08:22:25 -1000 Subject: [PATCH 059/636] remove concept of non-default sources --- README.md | 15 ++++++++++- .../headers/content_security_policy.rb | 25 +++---------------- .../headers/content_security_policy_spec.rb | 12 --------- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 75623826..a07ad689 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,23 @@ The following methods are going to be called, unless they are provided in a `ski config.x_download_options = 'noopen' config.x_permitted_cross_domain_policies = 'none' config.csp = { - :default_src => "https: self", + :default_src => "https: 'self'", :enforce => proc {|controller| controller.current_user.enforce_csp? }, :frame_src => "https: http:.twimg.com http://itunes.apple.com", :img_src => "https:", + :connect_src => "wws:" + :font_src => "'self' data:", + :frame_src => "'self'", + :img_src => "mycdn.com data:", + :media_src => "utoob.com", + :object_src => "'self'", + :script_src => "'self'", + :style_src => "'unsafe-inline'", + :base_uri => "'self'", + :child_src => "'self'", + :form_action => "'self' github.com", + :frame_ancestors => "'none'", + :plugin_types => 'application/x-shockwave-flash', :report_uri => '//example.com/uri-directive' } config.hpkp = { diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index cf5493a2..d1da8259 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -20,26 +20,19 @@ module Constants :media_src, :object_src, :script_src, - :style_src - ] - - NON_DEFAULT_SOURCES = [ + :style_src, :base_uri, :child_src, :form_action, :frame_ancestors, - :plugin_types, - :referrer, - :reflected_xss + :plugin_types ] OTHER = [ :report_uri ] - SOURCE_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES - - ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER + ALL_DIRECTIVES = DIRECTIVES + OTHER CONFIG_KEY = :csp end @@ -111,7 +104,7 @@ def initialize(config=nil, options={}) @config = config.inject({}) do |hash, (key, value)| config_val = value.respond_to?(:call) ? value.call(@controller) : value - if SOURCE_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings + if DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings config_val = config_val.split if config_val.is_a? String if config_val.is_a?(Array) config_val = config_val.map do |val| @@ -191,7 +184,6 @@ def build_value append_http_additions unless ssl_request? header_value = [ generic_directives, - non_default_directives, report_uri_directive ].join.strip end @@ -258,15 +250,6 @@ def generic_directives header_value end - def non_default_directives - header_value = '' - NON_DEFAULT_SOURCES.each do |directive_name| - header_value += build_directive(directive_name) if @config[directive_name] - end - - header_value - end - def build_directive(key) "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; " end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 7e6df031..fbe1bdd4 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -148,18 +148,6 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} }.to raise_error(RuntimeError) end - context "CSP level 2 directives" do - let(:config) { {:default_src => 'self'} } - ::SecureHeaders::ContentSecurityPolicy::Constants::NON_DEFAULT_SOURCES.each do |non_default_source| - it "supports all level 2 directives" do - directive_name = ::SecureHeaders::ContentSecurityPolicy.send(:symbol_to_hyphen_case, non_default_source) - config.merge!({ non_default_source => "value" }) - csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME)) - expect(csp.value).to match(/#{directive_name} value;/) - end - end - end - context "auto-whitelists data: uris for img-src" do it "sets the value if no img-src specified" do csp = ContentSecurityPolicy.new({:default_src => 'self'}, :request => request_for(CHROME)) From e6e25a5fe4729aebfe4d339e1a6311229b3bf8bc Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 08:30:06 -1000 Subject: [PATCH 060/636] clean up deprecation warnings --- README.md | 12 +++---- .../config/initializers/secure_headers.rb | 4 +-- .../controllers/other_things_controller.rb | 4 +-- .../config/initializers/secure_headers.rb | 4 +-- .../headers/content_security_policy_spec.rb | 32 +++++++++---------- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index a07ad689..1656e7dd 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ This configuration will likely work for most applications without modification. # Auction site wants to allow images from anywhere, plugin content from a list of trusted media providers (including a content distribution network), and scripts only from its server hosting sanitized JavaScript :csp => { - :default_src => 'self', + :default_src => "'self'", :img_src => '*', :object_src => ['media1.com', 'media2.com', '*.cdn.com'], # alternatively (NOT csv) :object_src => 'media1.com media2.com *.cdn.com' @@ -204,8 +204,8 @@ Setting a nonce will also set 'unsafe-inline' for browsers that don't support no ```ruby :csp => { - :default_src => 'self', - :script_src => 'self nonce' + :default_src => "'self'", + :script_src => "'self' nonce" } ``` @@ -251,7 +251,7 @@ If you only have a few hashes, you can hardcode them for the entire app: ```ruby config.csp = { :default_src => "https:", - :script_src => 'self' + :script_src => "'self'" :script_hashes => ['sha1-abc', 'sha1-qwe'] } ``` @@ -261,7 +261,7 @@ The following will work as well, but may not be as clear: ```ruby config.csp = { :default_src => "https:", - :script_src => "self 'sha1-qwe'" + :script_src => "'self' 'sha1-qwe'" } ``` @@ -276,7 +276,7 @@ use ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware ```ruby config.csp = { :default_src => "https:", - :script_src => 'self', + :script_src => "'self'", :script_hash_middleware => true } ``` diff --git a/fixtures/rails_3_2_22/config/initializers/secure_headers.rb b/fixtures/rails_3_2_22/config/initializers/secure_headers.rb index 9320dbf0..246c549c 100644 --- a/fixtures/rails_3_2_22/config/initializers/secure_headers.rb +++ b/fixtures/rails_3_2_22/config/initializers/secure_headers.rb @@ -5,8 +5,8 @@ config.x_xss_protection = {:value => 1, :mode => 'block'} config.x_permitted_cross_domain_policies = 'none' csp = { - :default_src => "self", - :script_src => "self nonce", + :default_src => "'self'", + :script_src => "'self' nonce", :report_uri => 'somewhere', :script_hash_middleware => true, :enforce => false # false means warnings only diff --git a/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb b/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb index 72211331..c92b808b 100644 --- a/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb +++ b/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb @@ -1,5 +1,5 @@ class OtherThingsController < ApplicationController - ensure_security_headers :csp => {:default_src => 'self'} + ensure_security_headers :csp => {:default_src => "'self'"} def index end @@ -11,7 +11,7 @@ def other_action def secure_header_options_for(header, options) if params[:action] == "other_action" if header == :csp - options.merge(:style_src => 'self') + options.merge(:style_src => "'self'") end else options diff --git a/fixtures/rails_4_1_8/config/initializers/secure_headers.rb b/fixtures/rails_4_1_8/config/initializers/secure_headers.rb index ea3eb847..350e8223 100644 --- a/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +++ b/fixtures/rails_4_1_8/config/initializers/secure_headers.rb @@ -5,8 +5,8 @@ config.x_xss_protection = {:value => 0} config.x_permitted_cross_domain_policies = 'none' csp = { - :default_src => "self", - :script_src => "self nonce", + :default_src => "'self'", + :script_src => "'self' nonce", :report_uri => 'somewhere', :script_hash_middleware => true, :enforce => false # false means warnings only diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index fbe1bdd4..3859371f 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -82,7 +82,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} describe "#normalize_csp_options" do before(:each) do - default_opts[:script_src] << ' self none' + default_opts[:script_src] << " 'self' 'none'" @opts = default_opts end @@ -106,7 +106,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "accepts procs for report-uris" do opts = { - :default_src => 'self', + :default_src => "'self'", :report_uri => proc { "http://lambda/result" } } @@ -131,7 +131,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} allow(controller).to receive(:current_user).and_return(user) opts = { - :default_src => "self", + :default_src => "'self'", :enforce => lambda { |c| c.current_user.beta_testing? } } csp = ContentSecurityPolicy.new(opts, :controller => controller) @@ -150,22 +150,22 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} context "auto-whitelists data: uris for img-src" do it "sets the value if no img-src specified" do - csp = ContentSecurityPolicy.new({:default_src => 'self'}, :request => request_for(CHROME)) + csp = ContentSecurityPolicy.new({:default_src => "'self'"}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") end it "appends the value if img-src is specified" do - csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self'}, :request => request_for(CHROME)) + csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self'"}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") end it "doesn't add a duplicate data uri if img-src specifies it already" do - csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self data:'}, :request => request_for(CHROME)) + csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self' data:"}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") end it "allows the user to disable img-src data: uris auto-whitelisting" do - csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_img_src_data_uri => true}, :request => request_for(CHROME)) + csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self'", :disable_img_src_data_uri => true}, :request => request_for(CHROME)) expect(csp.value).to eq("default-src 'self'; img-src 'self';") end end @@ -191,39 +191,39 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} context "when using a nonce" do it "adds a nonce and unsafe-inline to the script-src value when using chrome" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(CHROME), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end it "adds a nonce and unsafe-inline to the script-src value when using firefox" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(FIREFOX), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(FIREFOX), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end it "adds a nonce and unsafe-inline to the script-src value when using opera" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(OPERA), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(OPERA), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end it "does not add a nonce and unsafe-inline to the script-src value when using Safari" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(SAFARI), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(SAFARI), :controller => controller) expect(header.value).to include("script-src 'self' 'unsafe-inline'") expect(header.value).not_to include("nonce") end it "does not add a nonce and unsafe-inline to the script-src value when using IE" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(IE), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(IE), :controller => controller) expect(header.value).to include("script-src 'self' 'unsafe-inline'") expect(header.value).not_to include("nonce") end it "adds a nonce and unsafe-inline to the style-src value" do - header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "'self' nonce"), :request => request_for(CHROME), :controller => controller) expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") end it "adds an identical nonce to the style and script-src directives" do - header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "'self' nonce", :script_src => "'self' nonce"), :request => request_for(CHROME), :controller => controller) nonce = header.nonce value = header.value expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'") @@ -231,7 +231,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} end it "does not add 'unsafe-inline' twice" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce 'unsafe-inline'"), :request => request_for(CHROME), :controller => controller) + header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce 'unsafe-inline'"), :request => request_for(CHROME), :controller => controller) expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';") end end @@ -279,7 +279,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} describe ".add_to_env" do let(:controller) { double } - let(:config) { {:default_src => 'self'} } + let(:config) { {:default_src => "'self'"} } let(:options) { {:controller => controller} } it "adds metadata to env" do From bb9ebc6c12a677aad29af8e0f08ffd1def56efec Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 08:54:59 -1000 Subject: [PATCH 061/636] Document per controll/action config options --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1656e7dd..9a3edcbd 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ The following methods are going to be called, unless they are provided in a `ski **Place the following in an initializer (recommended):** +**NOTE: All CSP config values accept procs for one way of dynamically setting values** + ```ruby ::SecureHeaders::Configuration.configure do |config| config.hsts = {:max_age => 20.years.to_i, :include_subdomains => true} @@ -43,7 +45,7 @@ The following methods are going to be called, unless they are provided in a `ski config.x_permitted_cross_domain_policies = 'none' config.csp = { :default_src => "https: 'self'", - :enforce => proc {|controller| controller.current_user.enforce_csp? }, + :enforce => proc {|controller| controller.my_feature_flag_api.enabled? }, :frame_src => "https: http:.twimg.com http://itunes.apple.com", :img_src => "https:", :connect_src => "wws:" @@ -88,6 +90,37 @@ ensure_security_headers( ) ``` +## Per-action configuration + +Sometimes you need to override your content security policy for a given endpoint. Rather than applying the exception globally, you have a few options: + +1. Use procs as config values as mentioned above. +1. Specifying `ensure_security_headers csp: ::SecureHeaders::Configuration.csp.merge(script_src: shadyhost.com)` in a descendent controller will override the settings for that controller only. +1. Override the `secure_header_options_for` class instance method. e.g. + +```ruby +class SomethingController < ApplicationController + def wumbus + # gets style-src override + end + + def diffendoofer + # does not get style-src override + end + + def secure_header_options_for(header, options) + options = super + if params[:action] == "wumbus" + if header == :csp + options.merge(style_src: "'self'") + end + else + options + end + end +end +``` + ## Options for ensure\_security\_headers **To disable any of these headers, supply a value of false (e.g. :hsts => false), supplying nil will set the default value** From b5fe9d408d240d9dc99065629b665f35286db511 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Oct 2015 12:47:40 -1000 Subject: [PATCH 062/636] bump to 2.4.0 --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index db07b903..54391a0c 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.3.0" + VERSION = "2.4.0" end From 46764faf040396d99dfd16323f98f11cffb92bc5 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 5 Oct 2015 12:25:21 -1000 Subject: [PATCH 063/636] wip --- Gemfile | 1 + README.md | 3 +- .../headers/content_security_policy.rb | 134 ++++++++++++------ .../headers/content_security_policy_spec.rb | 8 +- 4 files changed, 95 insertions(+), 51 deletions(-) diff --git a/Gemfile b/Gemfile index 93a2a150..e2ab570a 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source 'https://rubygems.org' gemspec group :test do + gem 'pry' gem 'test-unit', '~> 3.0' gem 'rails', '3.2.22' gem 'sqlite3', :platforms => [:ruby, :mswin, :mingw] diff --git a/README.md b/README.md index 9a3edcbd..f4f5d0c5 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ The following methods are going to be called, unless they are provided in a `ski :form_action => "'self' github.com", :frame_ancestors => "'none'", :plugin_types => 'application/x-shockwave-flash', + :block_all_mixed_content => '' # see [http://www.w3.org/TR/mixed-content/]() :report_uri => '//example.com/uri-directive' } config.hpkp = { @@ -99,7 +100,7 @@ Sometimes you need to override your content security policy for a given endpoint 1. Override the `secure_header_options_for` class instance method. e.g. ```ruby -class SomethingController < ApplicationController +class SomethingController < ApplicationController def wumbus # gets style-src override end diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index d1da8259..32d67bea 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -3,6 +3,7 @@ require 'securerandom' require 'user_agent_parser' require 'json' +require 'pry' module SecureHeaders class ContentSecurityPolicyBuildError < StandardError; end @@ -11,7 +12,8 @@ module Constants DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:" HEADER_NAME = "Content-Security-Policy" ENV_KEY = 'secure_headers.content_security_policy' - DIRECTIVES = [ + + DIRECTIVES_1_0 = [ :default_src, :connect_src, :font_src, @@ -19,20 +21,53 @@ module Constants :img_src, :media_src, :object_src, + :sandbox, :script_src, :style_src, - :base_uri, + :report_uri + ].freeze + + DIRECTIVES_2_0 = [ + DIRECTIVES_1_0, + :base_url, :child_src, :form_action, :frame_ancestors, :plugin_types - ] + ].flatten.freeze - OTHER = [ - :report_uri - ] - ALL_DIRECTIVES = DIRECTIVES + OTHER + # All the directives currently under consideration for CSP level 3. + # https://w3c.github.io/webappsec/specs/CSP2/ + DIRECTIVES_3_0 = [ + DIRECTIVES_2_0, + :manifest_src, + :reflected_xss + ].flatten.freeze + + # All the directives that are not currently in a formal spec, but have + # been implemented somewhere. + DIRECTIVES_DRAFT = [ + :block_all_mixed_content, + ].freeze + + SAFARI_DIRECTIVES = DIRECTIVES_1_0 + + FIREFOX_UNSUPPORTED_DIRECTIVES = [ + :block_all_mixed_content, + :child_src, + :plugin_types + ].freeze + + FIREFOX_DIRECTIVES = ( + DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES + ).freeze + + CHROME_DIRECTIVES = ( + DIRECTIVES_2_0 + DIRECTIVES_DRAFT + ).freeze + + ALL_DIRECTIVES = DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT CONFIG_KEY = :csp end @@ -99,33 +134,36 @@ def initialize(config=nil, options={}) @ua = options[:ua] @ssl_request = !!options.delete(:ssl) @request_uri = options.delete(:request_uri) + @http_additions = config.delete(:http_additions) + @app_name = config.delete(:app_name) + @enforce = !!config.delete(:enforce) + @disable_img_src_data_uri = !!config.delete(:disable_img_src_data_uri) + @tag_report_uri = !!config.delete(:tag_report_uri) + @script_hashes = config.delete(:script_hashes) || [] # Config values can be string, array, or lamdba values @config = config.inject({}) do |hash, (key, value)| config_val = value.respond_to?(:call) ? value.call(@controller) : value - - if DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings + if ContentSecurityPolicy::ALL_DIRECTIVES.include?(key.to_sym) # directives need to be normalized to arrays of strings config_val = config_val.split if config_val.is_a? String if config_val.is_a?(Array) config_val = config_val.map do |val| translate_dir_value(val) end.flatten.uniq end + else + raise ArgumentError.new("Unknown directive supplied: #{key}") end + hash[key] = config_val hash end - @http_additions = @config.delete(:http_additions) - @app_name = @config.delete(:app_name) - @report_uri = @config.delete(:report_uri) - @enforce = !!@config.delete(:enforce) - @disable_img_src_data_uri = !!@config.delete(:disable_img_src_data_uri) - @tag_report_uri = !!@config.delete(:tag_report_uri) - @script_hashes = @config.delete(:script_hashes) || [] - add_script_hashes if @script_hashes.any? + puts @config + strip_unsupported_directives + puts @config end ## @@ -160,13 +198,20 @@ def value def to_json build_value - @config.to_json.gsub(/(\w+)_src/, "\\1-src") + out = @config.inject({}) do |hash, (key, value)| + hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value + hash + end + puts out + out.to_json end def self.from_json(*json_configs) json_configs.inject({}) do |combined_config, one_config| - one_config = one_config.gsub(/(\w+)-src/, "\\1_src") - config = JSON.parse(one_config, :symbolize_names => true) + config = JSON.parse(one_config).inject({}) do |hash, (key, value)| + hash[key.gsub(/(\w+)-(\w+)/, "\\1_\\2").to_sym] = value + hash + end combined_config.merge(config) do |_, lhs, rhs| lhs | rhs end @@ -182,10 +227,7 @@ def add_script_hashes def build_value raise "Expected to find default_src directive value" unless @config[:default_src] append_http_additions unless ssl_request? - header_value = [ - generic_directives, - report_uri_directive - ].join.strip + generic_directives end def append_http_additions @@ -204,7 +246,7 @@ def translate_dir_value val warn "[DEPRECATION] using self/none may not be supported in the future. Instead use 'self'/'none' instead." "'#{val}'" elsif val == 'nonce' - if supports_nonces?(@ua) + if supports_nonces? self.class.set_nonce(@controller, nonce) ["'nonce-#{nonce}'", "'unsafe-inline'"] else @@ -215,25 +257,6 @@ def translate_dir_value val end end - def report_uri_directive - return '' if @report_uri.nil? - - if @report_uri.start_with?('//') - @report_uri = if @ssl_request - "https:" + @report_uri - else - "http:" + @report_uri - end - end - - if @tag_report_uri - @report_uri = "#{@report_uri}?enforce=#{@enforce}" - @report_uri += "&app_name=#{@app_name}" if @app_name - end - - "report-uri #{@report_uri};" - end - def generic_directives header_value = '' data_uri = @disable_img_src_data_uri ? [] : ["data:"] @@ -243,7 +266,7 @@ def generic_directives @config[:img_src] = @config[:default_src] + data_uri end - DIRECTIVES.each do |directive_name| + ALL_DIRECTIVES.each do |directive_name| header_value += build_directive(directive_name) if @config[directive_name] end @@ -254,8 +277,25 @@ def build_directive(key) "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; " end - def supports_nonces?(user_agent) - parsed_ua = UserAgentParser.parse(user_agent) + def strip_unsupported_directives + @config.select! { |key, _| supported_directives.include?(key) } + end + + def supported_directives + @supported_directives ||= case UserAgentParser.parse(@ua).family + when "Chrome" + CHROME_DIRECTIVES + when "Safari" + SAFARI_DIRECTIVES + when "Firefox" + FIREFOX_DIRECTIVES + else + DIRECTIVES_1_0 + end + end + + def supports_nonces? + parsed_ua = UserAgentParser.parse(@ua) ["Chrome", "Opera", "Firefox"].include?(parsed_ua.family) end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 3859371f..97fbc768 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -5,9 +5,10 @@ module SecureHeaders let(:default_opts) do { :default_src => 'https:', - :report_uri => '/csp_report', + :img_src => "https: data:", :script_src => "'unsafe-inline' 'unsafe-eval' https: data:", - :style_src => "'unsafe-inline' https: about:" + :style_src => "'unsafe-inline' https: about:", + :report_uri => '/csp_report' } end let(:controller) { DummyClass.new } @@ -58,7 +59,8 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "exports a policy to JSON" do policy = ContentSecurityPolicy.new(default_opts) - expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]}) + puts default_opts + expected = %({"default-src":["https:"],"img-src":["https:","data:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"report-uri":["/csp_report"]}) expect(policy.to_json).to eq(expected) end From a5f7d76d18e8b29f03c53d532042437f3a4ae380 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 5 Oct 2015 12:50:10 -1000 Subject: [PATCH 064/636] refactor and introduce supported directives --- .../headers/content_security_policy.rb | 43 +++++++++++++------ .../headers/content_security_policy_spec.rb | 2 +- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 32d67bea..db4fe6fa 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -67,7 +67,7 @@ module Constants DIRECTIVES_2_0 + DIRECTIVES_DRAFT ).freeze - ALL_DIRECTIVES = DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT + ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq CONFIG_KEY = :csp end @@ -136,7 +136,10 @@ def initialize(config=nil, options={}) @request_uri = options.delete(:request_uri) @http_additions = config.delete(:http_additions) @app_name = config.delete(:app_name) - @enforce = !!config.delete(:enforce) + @app_name = @app_name.call(@controller) if @app_name.respond_to?(:call) + @enforce = config.delete(:enforce) + @enforce = @enforce.call(@controller) if @enforce.respond_to?(:call) + @enforce = !!@enforce @disable_img_src_data_uri = !!config.delete(:disable_img_src_data_uri) @tag_report_uri = !!config.delete(:tag_report_uri) @script_hashes = config.delete(:script_hashes) || [] @@ -144,7 +147,7 @@ def initialize(config=nil, options={}) # Config values can be string, array, or lamdba values @config = config.inject({}) do |hash, (key, value)| config_val = value.respond_to?(:call) ? value.call(@controller) : value - if ContentSecurityPolicy::ALL_DIRECTIVES.include?(key.to_sym) # directives need to be normalized to arrays of strings + if ([:enforce, :app_name] + ContentSecurityPolicy::ALL_DIRECTIVES).include?(key.to_sym) # directives need to be normalized to arrays of strings config_val = config_val.split if config_val.is_a? String if config_val.is_a?(Array) config_val = config_val.map do |val| @@ -155,15 +158,31 @@ def initialize(config=nil, options={}) raise ArgumentError.new("Unknown directive supplied: #{key}") end - hash[key] = config_val hash end + # normalize and tag the report-uri + if @config[:report_uri] + @config[:report_uri] = @config[:report_uri].map do |report_uri| + if report_uri.start_with?('//') + report_uri = if @ssl_request + "https:" + report_uri + else + "http:" + report_uri + end + end + + if @tag_report_uri + report_uri = "#{report_uri}?enforce=#{@enforce}" + report_uri += "&app_name=#{@app_name}" if @app_name + end + report_uri + end + end + add_script_hashes if @script_hashes.any? - puts @config strip_unsupported_directives - puts @config end ## @@ -198,12 +217,10 @@ def value def to_json build_value - out = @config.inject({}) do |hash, (key, value)| + @config.inject({}) do |hash, (key, value)| hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value hash - end - puts out - out.to_json + end.to_json end def self.from_json(*json_configs) @@ -267,10 +284,12 @@ def generic_directives end ALL_DIRECTIVES.each do |directive_name| - header_value += build_directive(directive_name) if @config[directive_name] + if @config[directive_name] + header_value += build_directive(directive_name) + end end - header_value + header_value.strip end def build_directive(key) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 97fbc768..5337b82d 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -250,7 +250,7 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "adds directive values for headers on http" do csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME)) - expect(csp.value).to eq("default-src https:; frame-src http:; img-src http: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;") + expect(csp.value).to eq("default-src https:; frame-src http:; img-src https: data: http:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;") end it "does not add the directive values if requesting https" do From 07367a2276b1ce11eaf57e601fcd6212f3b73163 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 5 Oct 2015 13:04:55 -1000 Subject: [PATCH 065/636] bug fixes and tests for filtered directives --- .../headers/content_security_policy.rb | 9 +++++--- .../headers/content_security_policy_spec.rb | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index db4fe6fa..555ef6b8 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -67,7 +67,7 @@ module Constants DIRECTIVES_2_0 + DIRECTIVES_DRAFT ).freeze - ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq + ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.sort.uniq CONFIG_KEY = :csp end @@ -274,8 +274,9 @@ def translate_dir_value val end end + # ensures defualt_src is first and report_uri is last def generic_directives - header_value = '' + header_value = build_directive(:default_src) data_uri = @disable_img_src_data_uri ? [] : ["data:"] if @config[:img_src] @config[:img_src] = @config[:img_src] + data_uri unless @config[:img_src].include?('data:') @@ -283,12 +284,14 @@ def generic_directives @config[:img_src] = @config[:default_src] + data_uri end - ALL_DIRECTIVES.each do |directive_name| + (ALL_DIRECTIVES - [:default_src, :report_uri]).each do |directive_name| if @config[directive_name] header_value += build_directive(directive_name) end end + header_value += build_directive(:report_uri) if @config[:report_uri] + header_value.strip end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 5337b82d..246c7dbd 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -143,6 +143,27 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} end describe "#value" do + context "browser sniffing" do + let(:complex_opts) do + ALL_DIRECTIVES.inject({}) { |memo, directive| memo[directive] = "'self'"; memo }.merge(:block_all_mixed_content => '') + end + + it "does not filter any directives for Chrome" do + policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(CHROME)) + expect(policy.value).to eq("default-src 'self'; base-url 'self'; block-all-mixed-content ; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") + end + + it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do + policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(FIREFOX)) + expect(policy.value).to eq("default-src 'self'; base-url 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") + end + + it "filters base-url, blocked-all-mixed-content, child-src, form-action, frame-ancestors, and plugin-types for safari" do + policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(SAFARI)) + expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") + end + end + it "raises an exception when default-src is missing" do csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME)) expect { From d41d3ba0c30f31b39fb6c8b7e21d30b2dc5f52db Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 5 Oct 2015 13:07:24 -1000 Subject: [PATCH 066/636] more cleanup from stricter config settings --- lib/secure_headers/headers/content_security_policy.rb | 3 ++- lib/secure_headers/view_helper.rb | 1 - .../content_security_policy/script_hash_middleware_spec.rb | 1 - .../secure_headers/headers/content_security_policy_spec.rb | 1 - spec/lib/secure_headers_spec.rb | 4 ++-- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 555ef6b8..861ff3d6 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -68,6 +68,7 @@ module Constants ).freeze ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.sort.uniq + ALL_CONFIGS = [:enforce, :app_name, :script_hash_middleware] + ALL_DIRECTIVES CONFIG_KEY = :csp end @@ -147,7 +148,7 @@ def initialize(config=nil, options={}) # Config values can be string, array, or lamdba values @config = config.inject({}) do |hash, (key, value)| config_val = value.respond_to?(:call) ? value.call(@controller) : value - if ([:enforce, :app_name] + ContentSecurityPolicy::ALL_DIRECTIVES).include?(key.to_sym) # directives need to be normalized to arrays of strings + if ContentSecurityPolicy::ALL_CONFIGS.include?(key.to_sym) # directives need to be normalized to arrays of strings config_val = config_val.split if config_val.is_a? String if config_val.is_a?(Array) config_val = config_val.map do |val| diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index abdafde7..a519333d 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -27,7 +27,6 @@ def hashed_javascript_tag(raise_error_on_unrecognized_hash = false, &block) if raise_error_on_unrecognized_hash raise UnexpectedHashedScriptException.new(message) else - puts message request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value end end diff --git a/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb b/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb index 62dbbd19..2a4b7654 100644 --- a/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb @@ -10,7 +10,6 @@ module SecureHeaders let(:default_config) do { - :disable_fill_missing => true, :default_src => 'https://*', :report_uri => '/csp_report', :script_src => "'unsafe-inline' 'unsafe-eval' https://* data:", diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 246c7dbd..36aaf598 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -59,7 +59,6 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "exports a policy to JSON" do policy = ContentSecurityPolicy.new(default_opts) - puts default_opts expected = %({"default-src":["https:"],"img-src":["https:","data:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"report-uri":["/csp_report"]}) expect(policy.to_json).to eq(expected) end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index dbf9ac28..31378ed3 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -166,7 +166,7 @@ def expect_default_values(hash) end it "produces a hash of headers given a hash as config" do - hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:", :disable_fill_missing => true}) + hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:"}) expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;") expect_default_values(hash) end @@ -186,7 +186,7 @@ def expect_default_values(hash) } end - hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:", :disable_fill_missing => true}) + hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:"}) ::SecureHeaders::Configuration.configure do |config| config.hsts = nil config.hpkp = nil From bc25f5cdc57a18da0dc560e463ee53d3f50d7ac3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 5 Oct 2015 13:24:06 -1000 Subject: [PATCH 067/636] Revert some unnecessary changes that broke things --- Gemfile | 1 - .../headers/content_security_policy.rb | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index e2ab570a..93a2a150 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,6 @@ source 'https://rubygems.org' gemspec group :test do - gem 'pry' gem 'test-unit', '~> 3.0' gem 'rails', '3.2.22' gem 'sqlite3', :platforms => [:ruby, :mswin, :mingw] diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 861ff3d6..b816ac01 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -3,7 +3,6 @@ require 'securerandom' require 'user_agent_parser' require 'json' -require 'pry' module SecureHeaders class ContentSecurityPolicyBuildError < StandardError; end @@ -68,7 +67,6 @@ module Constants ).freeze ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.sort.uniq - ALL_CONFIGS = [:enforce, :app_name, :script_hash_middleware] + ALL_DIRECTIVES CONFIG_KEY = :csp end @@ -136,26 +134,26 @@ def initialize(config=nil, options={}) @ssl_request = !!options.delete(:ssl) @request_uri = options.delete(:request_uri) @http_additions = config.delete(:http_additions) + @disable_img_src_data_uri = !!config.delete(:disable_img_src_data_uri) + @tag_report_uri = !!config.delete(:tag_report_uri) + @script_hashes = config.delete(:script_hashes) || [] @app_name = config.delete(:app_name) @app_name = @app_name.call(@controller) if @app_name.respond_to?(:call) @enforce = config.delete(:enforce) @enforce = @enforce.call(@controller) if @enforce.respond_to?(:call) @enforce = !!@enforce - @disable_img_src_data_uri = !!config.delete(:disable_img_src_data_uri) - @tag_report_uri = !!config.delete(:tag_report_uri) - @script_hashes = config.delete(:script_hashes) || [] # Config values can be string, array, or lamdba values @config = config.inject({}) do |hash, (key, value)| config_val = value.respond_to?(:call) ? value.call(@controller) : value - if ContentSecurityPolicy::ALL_CONFIGS.include?(key.to_sym) # directives need to be normalized to arrays of strings + if ALL_DIRECTIVES.include?(key.to_sym) # directives need to be normalized to arrays of strings config_val = config_val.split if config_val.is_a? String if config_val.is_a?(Array) config_val = config_val.map do |val| translate_dir_value(val) end.flatten.uniq end - else + elsif key != :script_hash_middleware raise ArgumentError.new("Unknown directive supplied: #{key}") end @@ -219,7 +217,9 @@ def value def to_json build_value @config.inject({}) do |hash, (key, value)| - hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value + if ALL_DIRECTIVES.include?(key) + hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value + end hash end.to_json end From 370ada3ada367df83bb918341af1957db0b00b17 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 5 Oct 2015 18:38:52 -1000 Subject: [PATCH 068/636] s/base_url/base_uri --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index b816ac01..26ce5e5b 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -28,7 +28,7 @@ module Constants DIRECTIVES_2_0 = [ DIRECTIVES_1_0, - :base_url, + :base_uri, :child_src, :form_action, :frame_ancestors, From 3aae0d466769a061bdcdbba29a971a27be75e17c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 5 Oct 2015 18:40:10 -1000 Subject: [PATCH 069/636] micro optimization: sort the uniq set rather than sorting a set and calling uniq --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 26ce5e5b..73e81f09 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -66,7 +66,7 @@ module Constants DIRECTIVES_2_0 + DIRECTIVES_DRAFT ).freeze - ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.sort.uniq + ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort CONFIG_KEY = :csp end From d3aa8de81cdeebecd30632c20f661101ee2b2304 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 5 Oct 2015 18:41:14 -1000 Subject: [PATCH 070/636] s/base-url/base-uri --- .../secure_headers/headers/content_security_policy_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 36aaf598..d8fb6150 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -149,15 +149,15 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(CHROME)) - expect(policy.value).to eq("default-src 'self'; base-url 'self'; block-all-mixed-content ; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content ; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(FIREFOX)) - expect(policy.value).to eq("default-src 'self'; base-url 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") end - it "filters base-url, blocked-all-mixed-content, child-src, form-action, frame-ancestors, and plugin-types for safari" do + it "filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, and plugin-types for safari" do policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(SAFARI)) expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") end From 8d76eef30fb600a502160028e48da8afb14d6c6c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 14 Oct 2015 12:50:28 -1000 Subject: [PATCH 071/636] version bump for UA sniffing --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 54391a0c..5251d57f 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.4.0" + VERSION = "2.4.1" end From 441b7a1798f56fc3a4819aa780541a13734c8af9 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 16 Oct 2015 10:08:34 -1000 Subject: [PATCH 072/636] delete fixture apps --- Rakefile | 36 +-- fixtures/rails_3_2_22/.rspec | 1 - fixtures/rails_3_2_22/Gemfile | 6 - fixtures/rails_3_2_22/README.rdoc | 261 ------------------ fixtures/rails_3_2_22/Rakefile | 7 - .../app/controllers/application_controller.rb | 4 - .../controllers/other_things_controller.rb | 5 - .../app/controllers/things_controller.rb | 5 - fixtures/rails_3_2_22/app/models/.gitkeep | 0 .../app/views/layouts/application.html.erb | 11 - .../app/views/other_things/index.html.erb | 2 - .../app/views/things/index.html.erb | 1 - fixtures/rails_3_2_22/config.ru | 7 - fixtures/rails_3_2_22/config/application.rb | 14 - fixtures/rails_3_2_22/config/boot.rb | 6 - fixtures/rails_3_2_22/config/environment.rb | 5 - .../rails_3_2_22/config/environments/test.rb | 37 --- .../config/initializers/secure_headers.rb | 16 -- fixtures/rails_3_2_22/config/routes.rb | 4 - .../rails_3_2_22/config/script_hashes.yml | 5 - fixtures/rails_3_2_22/lib/assets/.gitkeep | 0 fixtures/rails_3_2_22/lib/tasks/.gitkeep | 0 fixtures/rails_3_2_22/log/.gitkeep | 0 .../other_things_controller_spec.rb | 83 ------ .../controllers/things_controller_spec.rb | 54 ---- fixtures/rails_3_2_22/spec/spec_helper.rb | 15 - .../vendor/assets/javascripts/.gitkeep | 0 .../vendor/assets/stylesheets/.gitkeep | 0 fixtures/rails_3_2_22/vendor/plugins/.gitkeep | 0 fixtures/rails_3_2_22_no_init/.rspec | 1 - fixtures/rails_3_2_22_no_init/Gemfile | 6 - fixtures/rails_3_2_22_no_init/README.rdoc | 261 ------------------ fixtures/rails_3_2_22_no_init/Rakefile | 7 - .../app/controllers/application_controller.rb | 4 - .../controllers/other_things_controller.rb | 20 -- .../app/controllers/things_controller.rb | 5 - .../rails_3_2_22_no_init/app/models/.gitkeep | 0 .../app/views/layouts/application.html.erb | 12 - .../app/views/other_things/index.html.erb | 1 - .../app/views/things/index.html.erb | 0 fixtures/rails_3_2_22_no_init/config.ru | 4 - .../config/application.rb | 17 -- fixtures/rails_3_2_22_no_init/config/boot.rb | 6 - .../config/environment.rb | 5 - .../config/environments/test.rb | 37 --- .../rails_3_2_22_no_init/config/routes.rb | 4 - .../rails_3_2_22_no_init/lib/assets/.gitkeep | 0 .../rails_3_2_22_no_init/lib/tasks/.gitkeep | 0 fixtures/rails_3_2_22_no_init/log/.gitkeep | 0 .../other_things_controller_spec.rb | 56 ---- .../controllers/things_controller_spec.rb | 54 ---- .../rails_3_2_22_no_init/spec/spec_helper.rb | 5 - .../vendor/assets/javascripts/.gitkeep | 0 .../vendor/assets/stylesheets/.gitkeep | 0 .../vendor/plugins/.gitkeep | 0 fixtures/rails_4_1_8/Gemfile | 5 - fixtures/rails_4_1_8/README.rdoc | 28 -- fixtures/rails_4_1_8/Rakefile | 6 - .../app/controllers/application_controller.rb | 4 - .../app/controllers/concerns/.keep | 0 .../controllers/other_things_controller.rb | 5 - .../app/controllers/things_controller.rb | 5 - fixtures/rails_4_1_8/app/models/.keep | 0 .../rails_4_1_8/app/models/concerns/.keep | 0 .../app/views/layouts/application.html.erb | 11 - .../app/views/other_things/index.html.erb | 2 - .../app/views/things/index.html.erb | 1 - fixtures/rails_4_1_8/config.ru | 4 - fixtures/rails_4_1_8/config/application.rb | 15 - fixtures/rails_4_1_8/config/boot.rb | 4 - fixtures/rails_4_1_8/config/environment.rb | 5 - .../rails_4_1_8/config/environments/test.rb | 10 - .../config/initializers/secure_headers.rb | 16 -- fixtures/rails_4_1_8/config/routes.rb | 4 - fixtures/rails_4_1_8/config/script_hashes.yml | 5 - fixtures/rails_4_1_8/config/secrets.yml | 22 -- fixtures/rails_4_1_8/lib/assets/.keep | 0 fixtures/rails_4_1_8/lib/tasks/.keep | 0 fixtures/rails_4_1_8/log/.keep | 0 .../other_things_controller_spec.rb | 83 ------ .../controllers/things_controller_spec.rb | 59 ---- fixtures/rails_4_1_8/spec/spec_helper.rb | 15 - .../vendor/assets/javascripts/.keep | 0 .../vendor/assets/stylesheets/.keep | 0 84 files changed, 1 insertion(+), 1398 deletions(-) delete mode 100644 fixtures/rails_3_2_22/.rspec delete mode 100644 fixtures/rails_3_2_22/Gemfile delete mode 100644 fixtures/rails_3_2_22/README.rdoc delete mode 100644 fixtures/rails_3_2_22/Rakefile delete mode 100644 fixtures/rails_3_2_22/app/controllers/application_controller.rb delete mode 100644 fixtures/rails_3_2_22/app/controllers/other_things_controller.rb delete mode 100644 fixtures/rails_3_2_22/app/controllers/things_controller.rb delete mode 100644 fixtures/rails_3_2_22/app/models/.gitkeep delete mode 100644 fixtures/rails_3_2_22/app/views/layouts/application.html.erb delete mode 100644 fixtures/rails_3_2_22/app/views/other_things/index.html.erb delete mode 100644 fixtures/rails_3_2_22/app/views/things/index.html.erb delete mode 100644 fixtures/rails_3_2_22/config.ru delete mode 100644 fixtures/rails_3_2_22/config/application.rb delete mode 100644 fixtures/rails_3_2_22/config/boot.rb delete mode 100644 fixtures/rails_3_2_22/config/environment.rb delete mode 100644 fixtures/rails_3_2_22/config/environments/test.rb delete mode 100644 fixtures/rails_3_2_22/config/initializers/secure_headers.rb delete mode 100644 fixtures/rails_3_2_22/config/routes.rb delete mode 100644 fixtures/rails_3_2_22/config/script_hashes.yml delete mode 100644 fixtures/rails_3_2_22/lib/assets/.gitkeep delete mode 100644 fixtures/rails_3_2_22/lib/tasks/.gitkeep delete mode 100644 fixtures/rails_3_2_22/log/.gitkeep delete mode 100644 fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb delete mode 100644 fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb delete mode 100644 fixtures/rails_3_2_22/spec/spec_helper.rb delete mode 100644 fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep delete mode 100644 fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep delete mode 100644 fixtures/rails_3_2_22/vendor/plugins/.gitkeep delete mode 100644 fixtures/rails_3_2_22_no_init/.rspec delete mode 100644 fixtures/rails_3_2_22_no_init/Gemfile delete mode 100644 fixtures/rails_3_2_22_no_init/README.rdoc delete mode 100644 fixtures/rails_3_2_22_no_init/Rakefile delete mode 100644 fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb delete mode 100644 fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb delete mode 100644 fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb delete mode 100644 fixtures/rails_3_2_22_no_init/app/models/.gitkeep delete mode 100644 fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb delete mode 100644 fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb delete mode 100644 fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb delete mode 100644 fixtures/rails_3_2_22_no_init/config.ru delete mode 100644 fixtures/rails_3_2_22_no_init/config/application.rb delete mode 100644 fixtures/rails_3_2_22_no_init/config/boot.rb delete mode 100644 fixtures/rails_3_2_22_no_init/config/environment.rb delete mode 100644 fixtures/rails_3_2_22_no_init/config/environments/test.rb delete mode 100644 fixtures/rails_3_2_22_no_init/config/routes.rb delete mode 100644 fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep delete mode 100644 fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep delete mode 100644 fixtures/rails_3_2_22_no_init/log/.gitkeep delete mode 100644 fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb delete mode 100644 fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb delete mode 100644 fixtures/rails_3_2_22_no_init/spec/spec_helper.rb delete mode 100644 fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep delete mode 100644 fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep delete mode 100644 fixtures/rails_3_2_22_no_init/vendor/plugins/.gitkeep delete mode 100644 fixtures/rails_4_1_8/Gemfile delete mode 100644 fixtures/rails_4_1_8/README.rdoc delete mode 100644 fixtures/rails_4_1_8/Rakefile delete mode 100644 fixtures/rails_4_1_8/app/controllers/application_controller.rb delete mode 100644 fixtures/rails_4_1_8/app/controllers/concerns/.keep delete mode 100644 fixtures/rails_4_1_8/app/controllers/other_things_controller.rb delete mode 100644 fixtures/rails_4_1_8/app/controllers/things_controller.rb delete mode 100644 fixtures/rails_4_1_8/app/models/.keep delete mode 100644 fixtures/rails_4_1_8/app/models/concerns/.keep delete mode 100644 fixtures/rails_4_1_8/app/views/layouts/application.html.erb delete mode 100644 fixtures/rails_4_1_8/app/views/other_things/index.html.erb delete mode 100644 fixtures/rails_4_1_8/app/views/things/index.html.erb delete mode 100644 fixtures/rails_4_1_8/config.ru delete mode 100644 fixtures/rails_4_1_8/config/application.rb delete mode 100644 fixtures/rails_4_1_8/config/boot.rb delete mode 100644 fixtures/rails_4_1_8/config/environment.rb delete mode 100644 fixtures/rails_4_1_8/config/environments/test.rb delete mode 100644 fixtures/rails_4_1_8/config/initializers/secure_headers.rb delete mode 100644 fixtures/rails_4_1_8/config/routes.rb delete mode 100644 fixtures/rails_4_1_8/config/script_hashes.yml delete mode 100644 fixtures/rails_4_1_8/config/secrets.yml delete mode 100644 fixtures/rails_4_1_8/lib/assets/.keep delete mode 100644 fixtures/rails_4_1_8/lib/tasks/.keep delete mode 100644 fixtures/rails_4_1_8/log/.keep delete mode 100644 fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb delete mode 100644 fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb delete mode 100644 fixtures/rails_4_1_8/spec/spec_helper.rb delete mode 100644 fixtures/rails_4_1_8/vendor/assets/javascripts/.keep delete mode 100644 fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep diff --git a/Rakefile b/Rakefile index 581509a8..32612c79 100644 --- a/Rakefile +++ b/Rakefile @@ -10,41 +10,7 @@ RSpec::Core::RakeTask.new do |t| t.rspec_opts = "--format progress" end -task :default => :all_spec - -desc "Run all specs, and test fixture apps" -task :all_spec => :spec do - pwd = Dir.pwd - Dir.chdir 'fixtures/rails_3_2_22' - puts Dir.pwd - str = `bundle install >> /dev/null; bundle exec rspec spec` - puts str - unless $? == 0 - Dir.chdir pwd - fail "Header tests with app not using initializer failed exit code: #{$?}" - end - - Dir.chdir pwd - Dir.chdir 'fixtures/rails_3_2_22_no_init' - puts Dir.pwd - puts `bundle install >> /dev/null; bundle exec rspec spec` - - unless $? == 0 - fail "Header tests with app not using initializer failed" - Dir.chdir pwd - end - - Dir.chdir pwd - Dir.chdir 'fixtures/rails_4_1_8' - puts Dir.pwd - puts `bundle install >> /dev/null; bundle exec rspec spec` - - unless $? == 0 - fail "Header tests with Rails 4 failed" - Dir.chdir pwd - end - -end +task :default => :spec begin require 'rdoc/task' diff --git a/fixtures/rails_3_2_22/.rspec b/fixtures/rails_3_2_22/.rspec deleted file mode 100644 index 10205717..00000000 --- a/fixtures/rails_3_2_22/.rspec +++ /dev/null @@ -1 +0,0 @@ ---color --format progress diff --git a/fixtures/rails_3_2_22/Gemfile b/fixtures/rails_3_2_22/Gemfile deleted file mode 100644 index c674048a..00000000 --- a/fixtures/rails_3_2_22/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source 'https://rubygems.org' - -gem 'test-unit', '~> 3.0' -gem 'rails', '3.2.22' -gem 'rspec-rails', '>= 2.0.0' -gem 'secure_headers', :path => '../..' diff --git a/fixtures/rails_3_2_22/README.rdoc b/fixtures/rails_3_2_22/README.rdoc deleted file mode 100644 index 7c36f235..00000000 --- a/fixtures/rails_3_2_22/README.rdoc +++ /dev/null @@ -1,261 +0,0 @@ -== Welcome to Rails - -Rails is a web-application framework that includes everything needed to create -database-backed web applications according to the Model-View-Control pattern. - -This pattern splits the view (also called the presentation) into "dumb" -templates that are primarily responsible for inserting pre-built data in between -HTML tags. The model contains the "smart" domain objects (such as Account, -Product, Person, Post) that holds all the business logic and knows how to -persist themselves to a database. The controller handles the incoming requests -(such as Save New Account, Update Product, Show Post) by manipulating the model -and directing data to the view. - -In Rails, the model is handled by what's called an object-relational mapping -layer entitled Active Record. This layer allows you to present the data from -database rows as objects and embellish these data objects with business logic -methods. You can read more about Active Record in -link:files/vendor/rails/activerecord/README.html. - -The controller and view are handled by the Action Pack, which handles both -layers by its two parts: Action View and Action Controller. These two layers -are bundled in a single package due to their heavy interdependence. This is -unlike the relationship between the Active Record and Action Pack that is much -more separate. Each of these packages can be used independently outside of -Rails. You can read more about Action Pack in -link:files/vendor/rails/actionpack/README.html. - - -== Getting Started - -1. At the command prompt, create a new Rails application: - rails new myapp (where myapp is the application name) - -2. Change directory to myapp and start the web server: - cd myapp; rails server (run with --help for options) - -3. Go to http://localhost:3000/ and you'll see: - "Welcome aboard: You're riding Ruby on Rails!" - -4. Follow the guidelines to start developing your application. You can find -the following resources handy: - -* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html -* Ruby on Rails Tutorial Book: http://www.railstutorial.org/ - - -== Debugging Rails - -Sometimes your application goes wrong. Fortunately there are a lot of tools that -will help you debug it and get it back on the rails. - -First area to check is the application log files. Have "tail -f" commands -running on the server.log and development.log. Rails will automatically display -debugging and runtime information to these files. Debugging info will also be -shown in the browser on requests from 127.0.0.1. - -You can also log your own messages directly into the log file from your code -using the Ruby logger class from inside your controllers. Example: - - class WeblogController < ActionController::Base - def destroy - @weblog = Weblog.find(params[:id]) - @weblog.destroy - logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") - end - end - -The result will be a message in your log file along the lines of: - - Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! - -More information on how to use the logger is at http://www.ruby-doc.org/core/ - -Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are -several books available online as well: - -* Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) -* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) - -These two books will bring you up to speed on the Ruby language and also on -programming in general. - - -== Debugger - -Debugger support is available through the debugger command when you start your -Mongrel or WEBrick server with --debugger. This means that you can break out of -execution at any point in the code, investigate and change the model, and then, -resume execution! You need to install ruby-debug to run the server in debugging -mode. With gems, use sudo gem install ruby-debug. Example: - - class WeblogController < ActionController::Base - def index - @posts = Post.all - debugger - end - end - -So the controller will accept the action, run the first line, then present you -with a IRB prompt in the server window. Here you can do things like: - - >> @posts.inspect - => "[#nil, "body"=>nil, "id"=>"1"}>, - #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" - >> @posts.first.title = "hello from a debugger" - => "hello from a debugger" - -...and even better, you can examine how your runtime objects actually work: - - >> f = @posts.first - => #nil, "body"=>nil, "id"=>"1"}> - >> f. - Display all 152 possibilities? (y or n) - -Finally, when you're ready to resume execution, you can enter "cont". - - -== Console - -The console is a Ruby shell, which allows you to interact with your -application's domain model. Here you'll have all parts of the application -configured, just like it is when the application is running. You can inspect -domain models, change values, and save to the database. Starting the script -without arguments will launch it in the development environment. - -To start the console, run rails console from the application -directory. - -Options: - -* Passing the -s, --sandbox argument will rollback any modifications - made to the database. -* Passing an environment name as an argument will load the corresponding - environment. Example: rails console production. - -To reload your controllers and models after launching the console run -reload! - -More information about irb can be found at: -link:http://www.rubycentral.org/pickaxe/irb.html - - -== dbconsole - -You can go to the command line of your database directly through rails -dbconsole. You would be connected to the database with the credentials -defined in database.yml. Starting the script without arguments will connect you -to the development database. Passing an argument will connect you to a different -database, like rails dbconsole production. Currently works for MySQL, -PostgreSQL and SQLite 3. - -== Description of Contents - -The default directory structure of a generated Ruby on Rails application: - - |-- app - | |-- assets - | |-- images - | |-- javascripts - | `-- stylesheets - | |-- controllers - | |-- helpers - | |-- mailers - | |-- models - | `-- views - | `-- layouts - |-- config - | |-- environments - | |-- initializers - | `-- locales - |-- db - |-- doc - |-- lib - | `-- tasks - |-- log - |-- public - |-- script - |-- test - | |-- fixtures - | |-- functional - | |-- integration - | |-- performance - | `-- unit - |-- tmp - | |-- cache - | |-- pids - | |-- sessions - | `-- sockets - `-- vendor - |-- assets - `-- stylesheets - `-- plugins - -app - Holds all the code that's specific to this particular application. - -app/assets - Contains subdirectories for images, stylesheets, and JavaScript files. - -app/controllers - Holds controllers that should be named like weblogs_controller.rb for - automated URL mapping. All controllers should descend from - ApplicationController which itself descends from ActionController::Base. - -app/models - Holds models that should be named like post.rb. Models descend from - ActiveRecord::Base by default. - -app/views - Holds the template files for the view that should be named like - weblogs/index.html.erb for the WeblogsController#index action. All views use - eRuby syntax by default. - -app/views/layouts - Holds the template files for layouts to be used with views. This models the - common header/footer method of wrapping views. In your views, define a layout - using the layout :default and create a file named default.html.erb. - Inside default.html.erb, call <% yield %> to render the view using this - layout. - -app/helpers - Holds view helpers that should be named like weblogs_helper.rb. These are - generated for you automatically when using generators for controllers. - Helpers can be used to wrap functionality for your views into methods. - -config - Configuration files for the Rails environment, the routing map, the database, - and other dependencies. - -db - Contains the database schema in schema.rb. db/migrate contains all the - sequence of Migrations for your schema. - -doc - This directory is where your application documentation will be stored when - generated using rake doc:app - -lib - Application specific libraries. Basically, any kind of custom code that - doesn't belong under controllers, models, or helpers. This directory is in - the load path. - -public - The directory available for the web server. Also contains the dispatchers and the - default HTML files. This should be set as the DOCUMENT_ROOT of your web - server. - -script - Helper scripts for automation and generation. - -test - Unit and functional tests along with fixtures. When using the rails generate - command, template test files will be generated for you and placed in this - directory. - -vendor - External libraries that the application depends on. Also includes the plugins - subdirectory. If the app has frozen rails, those gems also go here, under - vendor/rails/. This directory is in the load path. diff --git a/fixtures/rails_3_2_22/Rakefile b/fixtures/rails_3_2_22/Rakefile deleted file mode 100644 index 992520a8..00000000 --- a/fixtures/rails_3_2_22/Rakefile +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env rake -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('../config/application', __FILE__) - -Rails3212::Application.load_tasks diff --git a/fixtures/rails_3_2_22/app/controllers/application_controller.rb b/fixtures/rails_3_2_22/app/controllers/application_controller.rb deleted file mode 100644 index 48d43dcc..00000000 --- a/fixtures/rails_3_2_22/app/controllers/application_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ApplicationController < ActionController::Base - protect_from_forgery - ensure_security_headers -end diff --git a/fixtures/rails_3_2_22/app/controllers/other_things_controller.rb b/fixtures/rails_3_2_22/app/controllers/other_things_controller.rb deleted file mode 100644 index 98eca8c2..00000000 --- a/fixtures/rails_3_2_22/app/controllers/other_things_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class OtherThingsController < ApplicationController - def index - - end -end \ No newline at end of file diff --git a/fixtures/rails_3_2_22/app/controllers/things_controller.rb b/fixtures/rails_3_2_22/app/controllers/things_controller.rb deleted file mode 100644 index d9c93da7..00000000 --- a/fixtures/rails_3_2_22/app/controllers/things_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ThingsController < ApplicationController - ensure_security_headers :csp => false - def index - end -end diff --git a/fixtures/rails_3_2_22/app/models/.gitkeep b/fixtures/rails_3_2_22/app/models/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22/app/views/layouts/application.html.erb b/fixtures/rails_3_2_22/app/views/layouts/application.html.erb deleted file mode 100644 index 58e8afa6..00000000 --- a/fixtures/rails_3_2_22/app/views/layouts/application.html.erb +++ /dev/null @@ -1,11 +0,0 @@ - - - - Rails3212 - - - -<%= yield %> - - - diff --git a/fixtures/rails_3_2_22/app/views/other_things/index.html.erb b/fixtures/rails_3_2_22/app/views/other_things/index.html.erb deleted file mode 100644 index ec6958aa..00000000 --- a/fixtures/rails_3_2_22/app/views/other_things/index.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -index - diff --git a/fixtures/rails_3_2_22/app/views/things/index.html.erb b/fixtures/rails_3_2_22/app/views/things/index.html.erb deleted file mode 100644 index 5605472e..00000000 --- a/fixtures/rails_3_2_22/app/views/things/index.html.erb +++ /dev/null @@ -1 +0,0 @@ -things diff --git a/fixtures/rails_3_2_22/config.ru b/fixtures/rails_3_2_22/config.ru deleted file mode 100644 index 27a3d8b3..00000000 --- a/fixtures/rails_3_2_22/config.ru +++ /dev/null @@ -1,7 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require ::File.expand_path('../config/environment', __FILE__) -run Rails3212::Application - -require 'secure_headers/headers/content_security_policy/script_hash_middleware' -use ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware diff --git a/fixtures/rails_3_2_22/config/application.rb b/fixtures/rails_3_2_22/config/application.rb deleted file mode 100644 index abfabf58..00000000 --- a/fixtures/rails_3_2_22/config/application.rb +++ /dev/null @@ -1,14 +0,0 @@ -require File.expand_path('../boot', __FILE__) - -require "action_controller/railtie" -require "sprockets/railtie" - -if defined?(Bundler) - Bundler.require(*Rails.groups(:assets => %w(development test))) -end - -module Rails3212 - class Application < Rails::Application - - end -end diff --git a/fixtures/rails_3_2_22/config/boot.rb b/fixtures/rails_3_2_22/config/boot.rb deleted file mode 100644 index 4489e586..00000000 --- a/fixtures/rails_3_2_22/config/boot.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'rubygems' - -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) - -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) diff --git a/fixtures/rails_3_2_22/config/environment.rb b/fixtures/rails_3_2_22/config/environment.rb deleted file mode 100644 index 9d2d7485..00000000 --- a/fixtures/rails_3_2_22/config/environment.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Load the rails application -require File.expand_path('../application', __FILE__) - -# Initialize the rails application -Rails3212::Application.initialize! diff --git a/fixtures/rails_3_2_22/config/environments/test.rb b/fixtures/rails_3_2_22/config/environments/test.rb deleted file mode 100644 index 883d7612..00000000 --- a/fixtures/rails_3_2_22/config/environments/test.rb +++ /dev/null @@ -1,37 +0,0 @@ -Rails3212::Application.configure do - # Settings specified here will take precedence over those in config/application.rb - - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true - - # Configure static asset server for tests with Cache-Control for performance - config.serve_static_assets = true - config.static_cache_control = "public, max-age=3600" - - # Log error messages when you accidentally call methods on nil - config.whiny_nils = true - - # Show full error reports and disable caching - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Raise exceptions instead of rendering exception templates - config.action_dispatch.show_exceptions = false - - # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false - - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - # config.action_mailer.delivery_method = :test - - # Raise exception on mass assignment protection for Active Record models - # config.active_record.mass_assignment_sanitizer = :strict - - # Print deprecation notices to the stderr - config.active_support.deprecation = :stderr -end diff --git a/fixtures/rails_3_2_22/config/initializers/secure_headers.rb b/fixtures/rails_3_2_22/config/initializers/secure_headers.rb deleted file mode 100644 index 246c549c..00000000 --- a/fixtures/rails_3_2_22/config/initializers/secure_headers.rb +++ /dev/null @@ -1,16 +0,0 @@ -::SecureHeaders::Configuration.configure do |config| - config.hsts = { :max_age => 10.years.to_i.to_s, :include_subdomains => false } - config.x_frame_options = 'SAMEORIGIN' - config.x_content_type_options = "nosniff" - config.x_xss_protection = {:value => 1, :mode => 'block'} - config.x_permitted_cross_domain_policies = 'none' - csp = { - :default_src => "'self'", - :script_src => "'self' nonce", - :report_uri => 'somewhere', - :script_hash_middleware => true, - :enforce => false # false means warnings only - } - - config.csp = csp -end diff --git a/fixtures/rails_3_2_22/config/routes.rb b/fixtures/rails_3_2_22/config/routes.rb deleted file mode 100644 index 58b2398d..00000000 --- a/fixtures/rails_3_2_22/config/routes.rb +++ /dev/null @@ -1,4 +0,0 @@ -Rails3212::Application.routes.draw do - resources :things - match ':controller(/:action(/:id))(.:format)' -end diff --git a/fixtures/rails_3_2_22/config/script_hashes.yml b/fixtures/rails_3_2_22/config/script_hashes.yml deleted file mode 100644 index 63f5ebc3..00000000 --- a/fixtures/rails_3_2_22/config/script_hashes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -app/views/layouts/application.html.erb: -- sha256-VjDxT7saxd2FgaUQQTWw/jsTnvonaoCP/ACWDBTpyhU= -app/views/other_things/index.html.erb: -- sha256-ZXAcP8a0y1pPMTJW8pUr43c+XBkgYQBwHOPvXk9mq5A= diff --git a/fixtures/rails_3_2_22/lib/assets/.gitkeep b/fixtures/rails_3_2_22/lib/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22/lib/tasks/.gitkeep b/fixtures/rails_3_2_22/lib/tasks/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22/log/.gitkeep b/fixtures/rails_3_2_22/log/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb b/fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb deleted file mode 100644 index 99a5b488..00000000 --- a/fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'spec_helper' - -require 'secure_headers/headers/content_security_policy/script_hash_middleware' - -describe OtherThingsController, :type => :controller do - include Rack::Test::Methods - - def app - OtherThingsController.action(:index) - end - - def request(opts = {}) - options = opts.merge( - { - 'HTTPS' => 'on', - 'HTTP_USER_AGENT' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22" - } - ) - - - Rack::MockRequest.env_for('/', options) - end - - - describe "headers" do - before(:each) do - _, @env = app.call(request) - end - - it "sets the X-XSS-Protection header" do - get '/' - expect(@env['X-XSS-Protection']).to eq('1; mode=block') - end - - it "sets the X-Frame-Options header" do - get '/' - expect(@env['X-Frame-Options']).to eq('SAMEORIGIN') - end - - it "sets the CSP header with a local reference to a nonce" do - middleware = ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware.new(app) - _, env = middleware.call(request(@env)) - expect(env['Content-Security-Policy-Report-Only']).to match(/script-src[^;]*'nonce-[a-zA-Z0-9\+\/=]{44}'/) - end - - it "sets the required hashes to whitelist inline script" do - middleware = ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware.new(app) - _, env = middleware.call(request(@env)) - hashes = ['sha256-VjDxT7saxd2FgaUQQTWw/jsTnvonaoCP/ACWDBTpyhU=', 'sha256-ZXAcP8a0y1pPMTJW8pUr43c+XBkgYQBwHOPvXk9mq5A='] - hashes.each do |hash| - expect(env['Content-Security-Policy-Report-Only']).to include(hash) - end - end - - it "sets the Strict-Transport-Security header" do - get '/' - expect(@env['Strict-Transport-Security']).to eq("max-age=315576000") - end - - it "sets the X-Download-Options header" do - get '/' - expect(@env['X-Download-Options']).to eq('noopen') - end - - it "sets the X-Content-Type-Options header" do - get '/' - expect(@env['X-Content-Type-Options']).to eq("nosniff") - end - - it "sets the X-Permitted-Cross-Domain-Policies" do - get '/' - expect(@env['X-Permitted-Cross-Domain-Policies']).to eq("none") - end - - context "using IE" do - it "sets the X-Content-Type-Options header" do - @env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0" - get '/' - expect(@env['X-Content-Type-Options']).to eq("nosniff") - end - end - end -end diff --git a/fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb b/fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb deleted file mode 100644 index bf5a0cc7..00000000 --- a/fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -# This controller is meant to be something that inherits config from application controller -# all values are defaulted because no initializer is configured, and the values in app controller -# only provide csp => false - -describe ThingsController, :type => :controller do - describe "headers" do - it "sets the X-XSS-Protection header" do - get :index - expect(response.headers['X-XSS-Protection']).to eq('1; mode=block') - end - - it "sets the X-Frame-Options header" do - get :index - expect(response.headers['X-Frame-Options']).to eq('SAMEORIGIN') - end - - it "does not set CSP header" do - get :index - expect(response.headers['Content-Security-Policy-Report-Only']).to eq(nil) - end - - #mock ssl - it "sets the Strict-Transport-Security header" do - request.env['HTTPS'] = 'on' - get :index - expect(response.headers['Strict-Transport-Security']).to eq("max-age=315576000") - end - - it "sets the X-Download-Options header" do - get :index - expect(response.headers['X-Download-Options']).to eq('noopen') - end - - it "sets the X-Content-Type-Options header" do - get :index - expect(response.headers['X-Content-Type-Options']).to eq("nosniff") - end - - it "sets the X-Permitted-Cross-Domain-Policies" do - get :index - expect(response.headers['X-Permitted-Cross-Domain-Policies']).to eq("none") - end - - context "using IE" do - it "sets the X-Content-Type-Options header" do - request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0" - get :index - expect(response.headers['X-Content-Type-Options']).to eq("nosniff") - end - end - end -end diff --git a/fixtures/rails_3_2_22/spec/spec_helper.rb b/fixtures/rails_3_2_22/spec/spec_helper.rb deleted file mode 100644 index 569d69cb..00000000 --- a/fixtures/rails_3_2_22/spec/spec_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rubygems' - -#uncomment the following line to use spork with the debugger -#require 'spork/ext/ruby-debug' - -# Spork.prefork do - # Loading more in this block will cause your tests to run faster. However, - # if you change any configuration or code from libraries loaded here, you'll - # need to restart spork for it take effect. -# This file is copied to spec/ when you run 'rails generate rspec:install' - ENV["RAILS_ENV"] ||= 'test' - require File.expand_path("../../config/environment", __FILE__) - require 'rspec/rails' -# end - diff --git a/fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep b/fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep b/fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22/vendor/plugins/.gitkeep b/fixtures/rails_3_2_22/vendor/plugins/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22_no_init/.rspec b/fixtures/rails_3_2_22_no_init/.rspec deleted file mode 100644 index 10205717..00000000 --- a/fixtures/rails_3_2_22_no_init/.rspec +++ /dev/null @@ -1 +0,0 @@ ---color --format progress diff --git a/fixtures/rails_3_2_22_no_init/Gemfile b/fixtures/rails_3_2_22_no_init/Gemfile deleted file mode 100644 index 3cf56cf2..00000000 --- a/fixtures/rails_3_2_22_no_init/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source 'https://rubygems.org' - -gem 'test-unit' -gem 'rails', '3.2.22' -gem 'rspec-rails', '>= 2.0.0' -gem 'secure_headers', :path => '../..' diff --git a/fixtures/rails_3_2_22_no_init/README.rdoc b/fixtures/rails_3_2_22_no_init/README.rdoc deleted file mode 100644 index 7c36f235..00000000 --- a/fixtures/rails_3_2_22_no_init/README.rdoc +++ /dev/null @@ -1,261 +0,0 @@ -== Welcome to Rails - -Rails is a web-application framework that includes everything needed to create -database-backed web applications according to the Model-View-Control pattern. - -This pattern splits the view (also called the presentation) into "dumb" -templates that are primarily responsible for inserting pre-built data in between -HTML tags. The model contains the "smart" domain objects (such as Account, -Product, Person, Post) that holds all the business logic and knows how to -persist themselves to a database. The controller handles the incoming requests -(such as Save New Account, Update Product, Show Post) by manipulating the model -and directing data to the view. - -In Rails, the model is handled by what's called an object-relational mapping -layer entitled Active Record. This layer allows you to present the data from -database rows as objects and embellish these data objects with business logic -methods. You can read more about Active Record in -link:files/vendor/rails/activerecord/README.html. - -The controller and view are handled by the Action Pack, which handles both -layers by its two parts: Action View and Action Controller. These two layers -are bundled in a single package due to their heavy interdependence. This is -unlike the relationship between the Active Record and Action Pack that is much -more separate. Each of these packages can be used independently outside of -Rails. You can read more about Action Pack in -link:files/vendor/rails/actionpack/README.html. - - -== Getting Started - -1. At the command prompt, create a new Rails application: - rails new myapp (where myapp is the application name) - -2. Change directory to myapp and start the web server: - cd myapp; rails server (run with --help for options) - -3. Go to http://localhost:3000/ and you'll see: - "Welcome aboard: You're riding Ruby on Rails!" - -4. Follow the guidelines to start developing your application. You can find -the following resources handy: - -* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html -* Ruby on Rails Tutorial Book: http://www.railstutorial.org/ - - -== Debugging Rails - -Sometimes your application goes wrong. Fortunately there are a lot of tools that -will help you debug it and get it back on the rails. - -First area to check is the application log files. Have "tail -f" commands -running on the server.log and development.log. Rails will automatically display -debugging and runtime information to these files. Debugging info will also be -shown in the browser on requests from 127.0.0.1. - -You can also log your own messages directly into the log file from your code -using the Ruby logger class from inside your controllers. Example: - - class WeblogController < ActionController::Base - def destroy - @weblog = Weblog.find(params[:id]) - @weblog.destroy - logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") - end - end - -The result will be a message in your log file along the lines of: - - Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! - -More information on how to use the logger is at http://www.ruby-doc.org/core/ - -Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are -several books available online as well: - -* Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) -* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) - -These two books will bring you up to speed on the Ruby language and also on -programming in general. - - -== Debugger - -Debugger support is available through the debugger command when you start your -Mongrel or WEBrick server with --debugger. This means that you can break out of -execution at any point in the code, investigate and change the model, and then, -resume execution! You need to install ruby-debug to run the server in debugging -mode. With gems, use sudo gem install ruby-debug. Example: - - class WeblogController < ActionController::Base - def index - @posts = Post.all - debugger - end - end - -So the controller will accept the action, run the first line, then present you -with a IRB prompt in the server window. Here you can do things like: - - >> @posts.inspect - => "[#nil, "body"=>nil, "id"=>"1"}>, - #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" - >> @posts.first.title = "hello from a debugger" - => "hello from a debugger" - -...and even better, you can examine how your runtime objects actually work: - - >> f = @posts.first - => #nil, "body"=>nil, "id"=>"1"}> - >> f. - Display all 152 possibilities? (y or n) - -Finally, when you're ready to resume execution, you can enter "cont". - - -== Console - -The console is a Ruby shell, which allows you to interact with your -application's domain model. Here you'll have all parts of the application -configured, just like it is when the application is running. You can inspect -domain models, change values, and save to the database. Starting the script -without arguments will launch it in the development environment. - -To start the console, run rails console from the application -directory. - -Options: - -* Passing the -s, --sandbox argument will rollback any modifications - made to the database. -* Passing an environment name as an argument will load the corresponding - environment. Example: rails console production. - -To reload your controllers and models after launching the console run -reload! - -More information about irb can be found at: -link:http://www.rubycentral.org/pickaxe/irb.html - - -== dbconsole - -You can go to the command line of your database directly through rails -dbconsole. You would be connected to the database with the credentials -defined in database.yml. Starting the script without arguments will connect you -to the development database. Passing an argument will connect you to a different -database, like rails dbconsole production. Currently works for MySQL, -PostgreSQL and SQLite 3. - -== Description of Contents - -The default directory structure of a generated Ruby on Rails application: - - |-- app - | |-- assets - | |-- images - | |-- javascripts - | `-- stylesheets - | |-- controllers - | |-- helpers - | |-- mailers - | |-- models - | `-- views - | `-- layouts - |-- config - | |-- environments - | |-- initializers - | `-- locales - |-- db - |-- doc - |-- lib - | `-- tasks - |-- log - |-- public - |-- script - |-- test - | |-- fixtures - | |-- functional - | |-- integration - | |-- performance - | `-- unit - |-- tmp - | |-- cache - | |-- pids - | |-- sessions - | `-- sockets - `-- vendor - |-- assets - `-- stylesheets - `-- plugins - -app - Holds all the code that's specific to this particular application. - -app/assets - Contains subdirectories for images, stylesheets, and JavaScript files. - -app/controllers - Holds controllers that should be named like weblogs_controller.rb for - automated URL mapping. All controllers should descend from - ApplicationController which itself descends from ActionController::Base. - -app/models - Holds models that should be named like post.rb. Models descend from - ActiveRecord::Base by default. - -app/views - Holds the template files for the view that should be named like - weblogs/index.html.erb for the WeblogsController#index action. All views use - eRuby syntax by default. - -app/views/layouts - Holds the template files for layouts to be used with views. This models the - common header/footer method of wrapping views. In your views, define a layout - using the layout :default and create a file named default.html.erb. - Inside default.html.erb, call <% yield %> to render the view using this - layout. - -app/helpers - Holds view helpers that should be named like weblogs_helper.rb. These are - generated for you automatically when using generators for controllers. - Helpers can be used to wrap functionality for your views into methods. - -config - Configuration files for the Rails environment, the routing map, the database, - and other dependencies. - -db - Contains the database schema in schema.rb. db/migrate contains all the - sequence of Migrations for your schema. - -doc - This directory is where your application documentation will be stored when - generated using rake doc:app - -lib - Application specific libraries. Basically, any kind of custom code that - doesn't belong under controllers, models, or helpers. This directory is in - the load path. - -public - The directory available for the web server. Also contains the dispatchers and the - default HTML files. This should be set as the DOCUMENT_ROOT of your web - server. - -script - Helper scripts for automation and generation. - -test - Unit and functional tests along with fixtures. When using the rails generate - command, template test files will be generated for you and placed in this - directory. - -vendor - External libraries that the application depends on. Also includes the plugins - subdirectory. If the app has frozen rails, those gems also go here, under - vendor/rails/. This directory is in the load path. diff --git a/fixtures/rails_3_2_22_no_init/Rakefile b/fixtures/rails_3_2_22_no_init/Rakefile deleted file mode 100644 index 992520a8..00000000 --- a/fixtures/rails_3_2_22_no_init/Rakefile +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env rake -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('../config/application', __FILE__) - -Rails3212::Application.load_tasks diff --git a/fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb b/fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb deleted file mode 100644 index dcc4e74b..00000000 --- a/fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ApplicationController < ActionController::Base - protect_from_forgery - ensure_security_headers :csp => false -end diff --git a/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb b/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb deleted file mode 100644 index c92b808b..00000000 --- a/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -class OtherThingsController < ApplicationController - ensure_security_headers :csp => {:default_src => "'self'"} - def index - - end - - def other_action - render :text => 'yooooo' - end - - def secure_header_options_for(header, options) - if params[:action] == "other_action" - if header == :csp - options.merge(:style_src => "'self'") - end - else - options - end - end -end diff --git a/fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb b/fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb deleted file mode 100644 index 16ac413f..00000000 --- a/fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ThingsController < ApplicationController - def index - - end -end diff --git a/fixtures/rails_3_2_22_no_init/app/models/.gitkeep b/fixtures/rails_3_2_22_no_init/app/models/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb b/fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb deleted file mode 100644 index 72b7c3ff..00000000 --- a/fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb +++ /dev/null @@ -1,12 +0,0 @@ - - - - Rails3212 - <%= stylesheet_link_tag "application", :media => "all" %> - - - -<%= yield %> - - - diff --git a/fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb b/fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb deleted file mode 100644 index b2d525b2..00000000 --- a/fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb +++ /dev/null @@ -1 +0,0 @@ -index \ No newline at end of file diff --git a/fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb b/fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22_no_init/config.ru b/fixtures/rails_3_2_22_no_init/config.ru deleted file mode 100644 index 04ba316f..00000000 --- a/fixtures/rails_3_2_22_no_init/config.ru +++ /dev/null @@ -1,4 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require ::File.expand_path('../config/environment', __FILE__) -run Rails3212::Application diff --git a/fixtures/rails_3_2_22_no_init/config/application.rb b/fixtures/rails_3_2_22_no_init/config/application.rb deleted file mode 100644 index b80cd330..00000000 --- a/fixtures/rails_3_2_22_no_init/config/application.rb +++ /dev/null @@ -1,17 +0,0 @@ -require File.expand_path('../boot', __FILE__) - -# Pick the frameworks you want: -require "action_controller/railtie" -require "sprockets/railtie" - -if defined?(Bundler) - # If you precompile assets before deploying to production, use this line - Bundler.require(*Rails.groups(:assets => %w(development test))) - # If you want your assets lazily compiled in production, use this line - # Bundler.require(:default, :assets, Rails.env) -end - -module Rails3212 - class Application < Rails::Application - end -end diff --git a/fixtures/rails_3_2_22_no_init/config/boot.rb b/fixtures/rails_3_2_22_no_init/config/boot.rb deleted file mode 100644 index 4489e586..00000000 --- a/fixtures/rails_3_2_22_no_init/config/boot.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'rubygems' - -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) - -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) diff --git a/fixtures/rails_3_2_22_no_init/config/environment.rb b/fixtures/rails_3_2_22_no_init/config/environment.rb deleted file mode 100644 index 9d2d7485..00000000 --- a/fixtures/rails_3_2_22_no_init/config/environment.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Load the rails application -require File.expand_path('../application', __FILE__) - -# Initialize the rails application -Rails3212::Application.initialize! diff --git a/fixtures/rails_3_2_22_no_init/config/environments/test.rb b/fixtures/rails_3_2_22_no_init/config/environments/test.rb deleted file mode 100644 index 883d7612..00000000 --- a/fixtures/rails_3_2_22_no_init/config/environments/test.rb +++ /dev/null @@ -1,37 +0,0 @@ -Rails3212::Application.configure do - # Settings specified here will take precedence over those in config/application.rb - - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true - - # Configure static asset server for tests with Cache-Control for performance - config.serve_static_assets = true - config.static_cache_control = "public, max-age=3600" - - # Log error messages when you accidentally call methods on nil - config.whiny_nils = true - - # Show full error reports and disable caching - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Raise exceptions instead of rendering exception templates - config.action_dispatch.show_exceptions = false - - # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false - - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - # config.action_mailer.delivery_method = :test - - # Raise exception on mass assignment protection for Active Record models - # config.active_record.mass_assignment_sanitizer = :strict - - # Print deprecation notices to the stderr - config.active_support.deprecation = :stderr -end diff --git a/fixtures/rails_3_2_22_no_init/config/routes.rb b/fixtures/rails_3_2_22_no_init/config/routes.rb deleted file mode 100644 index 58b2398d..00000000 --- a/fixtures/rails_3_2_22_no_init/config/routes.rb +++ /dev/null @@ -1,4 +0,0 @@ -Rails3212::Application.routes.draw do - resources :things - match ':controller(/:action(/:id))(.:format)' -end diff --git a/fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep b/fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep b/fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22_no_init/log/.gitkeep b/fixtures/rails_3_2_22_no_init/log/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb b/fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb deleted file mode 100644 index ad6d2bf2..00000000 --- a/fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -describe OtherThingsController, :type => :controller do - describe "headers" do - it "sets the X-XSS-Protection header" do - get :index - expect(response.headers['X-XSS-Protection']).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE) - end - - it "sets the X-Frame-Options header" do - get :index - expect(response.headers['X-Frame-Options']).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) - end - - it "sets the CSP header" do - get :index - expect(response.headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; img-src 'self' data:;") - end - - it "sets per-action values based on secure_header_options_for" do - # munges :style_src => self into policy - get :other_action - expect(response.headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; img-src 'self' data:; style-src 'self';") - end - - #mock ssl - it "sets the Strict-Transport-Security header" do - request.env['HTTPS'] = 'on' - get :index - expect(response.headers['Strict-Transport-Security']).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE) - end - - it "sets the X-Download-Options header" do - get :index - expect(response.headers['X-Download-Options']).to eq(SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE) - end - - it "sets the X-Content-Type-Options header" do - get :index - expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) - end - - it "sets the X-Permitted-Cross-Domain-Policies" do - get :index - expect(response.headers['X-Permitted-Cross-Domain-Policies']).to eq("none") - end - - context "using IE" do - it "sets the X-Content-Type-Options header" do - request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0" - get :index - expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) - end - end - end -end diff --git a/fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb b/fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb deleted file mode 100644 index 09cd43e9..00000000 --- a/fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -# This controller is meant to be something that inherits config from application controller -# all values are defaulted because no initializer is configured, and the values in app controller -# only provide csp => false - -describe ThingsController, :type => :controller do - describe "headers" do - it "sets the X-XSS-Protection header" do - get :index - expect(response.headers['X-XSS-Protection']).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE) - end - - it "sets the X-Frame-Options header" do - get :index - expect(response.headers['X-Frame-Options']).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) - end - - it "sets the X-WebKit-CSP header" do - get :index - expect(response.headers['Content-Security-Policy-Report-Only']).to eq(nil) - end - - #mock ssl - it "sets the Strict-Transport-Security header" do - request.env['HTTPS'] = 'on' - get :index - expect(response.headers['Strict-Transport-Security']).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE) - end - - it "sets the X-Download-Options header" do - get :index - expect(response.headers['X-Download-Options']).to eq(SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE) - end - - it "sets the X-Content-Type-Options header" do - get :index - expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) - end - - it "sets the X-Permitted-Cross-Domain-Policies" do - get :index - expect(response.headers['X-Permitted-Cross-Domain-Policies']).to eq("none") - end - - context "using IE" do - it "sets the X-Content-Type-Options header" do - request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0" - get :index - expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) - end - end - end -end diff --git a/fixtures/rails_3_2_22_no_init/spec/spec_helper.rb b/fixtures/rails_3_2_22_no_init/spec/spec_helper.rb deleted file mode 100644 index d7d60e04..00000000 --- a/fixtures/rails_3_2_22_no_init/spec/spec_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rubygems' - -ENV["RAILS_ENV"] ||= 'test' -require File.expand_path("../../config/environment", __FILE__) -require 'rspec/rails' diff --git a/fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep b/fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep b/fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_3_2_22_no_init/vendor/plugins/.gitkeep b/fixtures/rails_3_2_22_no_init/vendor/plugins/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_4_1_8/Gemfile b/fixtures/rails_4_1_8/Gemfile deleted file mode 100644 index 1f734232..00000000 --- a/fixtures/rails_4_1_8/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -source 'https://rubygems.org' - -gem 'rails', '4.1.8' -gem 'rspec-rails', '>= 2.0.0' -gem 'secure_headers', :path => '../..' diff --git a/fixtures/rails_4_1_8/README.rdoc b/fixtures/rails_4_1_8/README.rdoc deleted file mode 100644 index dd4e97e2..00000000 --- a/fixtures/rails_4_1_8/README.rdoc +++ /dev/null @@ -1,28 +0,0 @@ -== README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... - - -Please feel free to use a different markup language if you do not plan to run -rake doc:app. diff --git a/fixtures/rails_4_1_8/Rakefile b/fixtures/rails_4_1_8/Rakefile deleted file mode 100644 index ba6b733d..00000000 --- a/fixtures/rails_4_1_8/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('../config/application', __FILE__) - -Rails.application.load_tasks diff --git a/fixtures/rails_4_1_8/app/controllers/application_controller.rb b/fixtures/rails_4_1_8/app/controllers/application_controller.rb deleted file mode 100644 index 48d43dcc..00000000 --- a/fixtures/rails_4_1_8/app/controllers/application_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ApplicationController < ActionController::Base - protect_from_forgery - ensure_security_headers -end diff --git a/fixtures/rails_4_1_8/app/controllers/concerns/.keep b/fixtures/rails_4_1_8/app/controllers/concerns/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb b/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb deleted file mode 100644 index 20b6ec9e..00000000 --- a/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class OtherThingsController < ApplicationController - def index - - end -end diff --git a/fixtures/rails_4_1_8/app/controllers/things_controller.rb b/fixtures/rails_4_1_8/app/controllers/things_controller.rb deleted file mode 100644 index d9c93da7..00000000 --- a/fixtures/rails_4_1_8/app/controllers/things_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ThingsController < ApplicationController - ensure_security_headers :csp => false - def index - end -end diff --git a/fixtures/rails_4_1_8/app/models/.keep b/fixtures/rails_4_1_8/app/models/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_4_1_8/app/models/concerns/.keep b/fixtures/rails_4_1_8/app/models/concerns/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_4_1_8/app/views/layouts/application.html.erb b/fixtures/rails_4_1_8/app/views/layouts/application.html.erb deleted file mode 100644 index 1e172377..00000000 --- a/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +++ /dev/null @@ -1,11 +0,0 @@ - - - - Rails418 - - - -<%= yield %> - - - diff --git a/fixtures/rails_4_1_8/app/views/other_things/index.html.erb b/fixtures/rails_4_1_8/app/views/other_things/index.html.erb deleted file mode 100644 index ec6958aa..00000000 --- a/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -index - diff --git a/fixtures/rails_4_1_8/app/views/things/index.html.erb b/fixtures/rails_4_1_8/app/views/things/index.html.erb deleted file mode 100644 index 5605472e..00000000 --- a/fixtures/rails_4_1_8/app/views/things/index.html.erb +++ /dev/null @@ -1 +0,0 @@ -things diff --git a/fixtures/rails_4_1_8/config.ru b/fixtures/rails_4_1_8/config.ru deleted file mode 100644 index 5bc2a619..00000000 --- a/fixtures/rails_4_1_8/config.ru +++ /dev/null @@ -1,4 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require ::File.expand_path('../config/environment', __FILE__) -run Rails.application diff --git a/fixtures/rails_4_1_8/config/application.rb b/fixtures/rails_4_1_8/config/application.rb deleted file mode 100644 index 004bfab5..00000000 --- a/fixtures/rails_4_1_8/config/application.rb +++ /dev/null @@ -1,15 +0,0 @@ -require File.expand_path('../boot', __FILE__) - -require "action_controller/railtie" -require "sprockets/railtie" - -# Require the gems listed in Gemfile, including any gems -# you've limited to :test, :development, or :production. -Bundler.require(*Rails.groups) - - -module Rails418 - class Application < Rails::Application - - end -end diff --git a/fixtures/rails_4_1_8/config/boot.rb b/fixtures/rails_4_1_8/config/boot.rb deleted file mode 100644 index 5e5f0c1f..00000000 --- a/fixtures/rails_4_1_8/config/boot.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) - -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/fixtures/rails_4_1_8/config/environment.rb b/fixtures/rails_4_1_8/config/environment.rb deleted file mode 100644 index ee8d90dc..00000000 --- a/fixtures/rails_4_1_8/config/environment.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Load the Rails application. -require File.expand_path('../application', __FILE__) - -# Initialize the Rails application. -Rails.application.initialize! diff --git a/fixtures/rails_4_1_8/config/environments/test.rb b/fixtures/rails_4_1_8/config/environments/test.rb deleted file mode 100644 index de5182ea..00000000 --- a/fixtures/rails_4_1_8/config/environments/test.rb +++ /dev/null @@ -1,10 +0,0 @@ -Rails418::Application.configure do - config.cache_classes = true - config.eager_load = false - config.serve_static_assets = true - config.static_cache_control = 'public, max-age=3600' - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - config.action_dispatch.show_exceptions = false - config.action_controller.allow_forgery_protection = false -end diff --git a/fixtures/rails_4_1_8/config/initializers/secure_headers.rb b/fixtures/rails_4_1_8/config/initializers/secure_headers.rb deleted file mode 100644 index 350e8223..00000000 --- a/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +++ /dev/null @@ -1,16 +0,0 @@ -::SecureHeaders::Configuration.configure do |config| - config.hsts = { :max_age => 10.years.to_i.to_s, :include_subdomains => false } - config.x_frame_options = 'DENY' - config.x_content_type_options = "nosniff" - config.x_xss_protection = {:value => 0} - config.x_permitted_cross_domain_policies = 'none' - csp = { - :default_src => "'self'", - :script_src => "'self' nonce", - :report_uri => 'somewhere', - :script_hash_middleware => true, - :enforce => false # false means warnings only - } - - config.csp = csp -end diff --git a/fixtures/rails_4_1_8/config/routes.rb b/fixtures/rails_4_1_8/config/routes.rb deleted file mode 100644 index cda02254..00000000 --- a/fixtures/rails_4_1_8/config/routes.rb +++ /dev/null @@ -1,4 +0,0 @@ -Rails.application.routes.draw do - resources :things - match ':controller(/:action(/:id))(.:format)', :via => [:get, :post] -end diff --git a/fixtures/rails_4_1_8/config/script_hashes.yml b/fixtures/rails_4_1_8/config/script_hashes.yml deleted file mode 100644 index 63f5ebc3..00000000 --- a/fixtures/rails_4_1_8/config/script_hashes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -app/views/layouts/application.html.erb: -- sha256-VjDxT7saxd2FgaUQQTWw/jsTnvonaoCP/ACWDBTpyhU= -app/views/other_things/index.html.erb: -- sha256-ZXAcP8a0y1pPMTJW8pUr43c+XBkgYQBwHOPvXk9mq5A= diff --git a/fixtures/rails_4_1_8/config/secrets.yml b/fixtures/rails_4_1_8/config/secrets.yml deleted file mode 100644 index c5bf116c..00000000 --- a/fixtures/rails_4_1_8/config/secrets.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key is used for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. - -# Make sure the secrets in this file are kept private -# if you're sharing your code publicly. - -development: - secret_key_base: ddba38f932720d8f18257f2a05dc278963a29cf569c45aa97ff4e9fc9bbc78af5a03fcf135caad45caee66ac09f8f9913c1f5e338a61213f420eefa8dd6363d2 - -test: - secret_key_base: f73abd7eab84fa7af5a2fc0a9c2727c5bad47433e51aa0c9c6b0782dac176a8e7f337e1f93adc6d6fc17027e67a533040b6408e54d72dea2eec6e5b9820dbcb9 - -# Do not keep production secrets in the repository, -# instead read values from the environment. -production: - secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/fixtures/rails_4_1_8/lib/assets/.keep b/fixtures/rails_4_1_8/lib/assets/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_4_1_8/lib/tasks/.keep b/fixtures/rails_4_1_8/lib/tasks/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_4_1_8/log/.keep b/fixtures/rails_4_1_8/log/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb b/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb deleted file mode 100644 index 6b602565..00000000 --- a/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'spec_helper' - -require 'secure_headers/headers/content_security_policy/script_hash_middleware' - -describe OtherThingsController, :type => :controller do - include Rack::Test::Methods - - def app - OtherThingsController.action(:index) - end - - def request(opts = {}) - options = opts.merge( - { - 'HTTPS' => 'on', - 'HTTP_USER_AGENT' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22" - } - ) - - - Rack::MockRequest.env_for('/', options) - end - - - describe "headers" do - before(:each) do - _, @env = app.call(request) - end - - it "sets the X-XSS-Protection header" do - get '/' - expect(@env['X-XSS-Protection']).to eq('0') - end - - it "sets the X-Frame-Options header" do - get '/' - expect(@env['X-Frame-Options']).to eq('DENY') - end - - it "sets the CSP header with a local reference to a nonce" do - middleware = ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware.new(app) - _, env = middleware.call(request(@env)) - expect(env['Content-Security-Policy-Report-Only']).to match(/script-src[^;]*'nonce-[a-zA-Z0-9\+\/=]{44}'/) - end - - it "sets the required hashes to whitelist inline script" do - middleware = ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware.new(app) - _, env = middleware.call(request(@env)) - hashes = ['sha256-VjDxT7saxd2FgaUQQTWw/jsTnvonaoCP/ACWDBTpyhU=', 'sha256-ZXAcP8a0y1pPMTJW8pUr43c+XBkgYQBwHOPvXk9mq5A='] - hashes.each do |hash| - expect(env['Content-Security-Policy-Report-Only']).to include(hash) - end - end - - it "sets the Strict-Transport-Security header" do - get '/' - expect(@env['Strict-Transport-Security']).to eq("max-age=315576000") - end - - it "sets the X-Download-Options header" do - get '/' - expect(@env['X-Download-Options']).to eq('noopen') - end - - it "sets the X-Content-Type-Options header" do - get '/' - expect(@env['X-Content-Type-Options']).to eq("nosniff") - end - - it "sets the X-Permitted-Cross-Domain-Policies" do - get '/' - expect(@env['X-Permitted-Cross-Domain-Policies']).to eq("none") - end - - context "using IE" do - it "sets the X-Content-Type-Options header" do - @env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0" - get '/' - expect(@env['X-Content-Type-Options']).to eq("nosniff") - end - end - end -end diff --git a/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb b/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb deleted file mode 100644 index febc3912..00000000 --- a/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# config.action_dispatch.default_headers defaults to: -# {"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff"} -# so we want to set our specs to expect something else to ensure secureheaders is taking precedence - -require 'spec_helper' - -# This controller is meant to be something that inherits config from application controller -# all values are defaulted because no initializer is configured, and the values in app controller -# only provide csp => false - -describe ThingsController, :type => :controller do - - describe "headers" do - it "sets the X-XSS-Protection header" do - get :index - expect(response.headers['X-XSS-Protection']).to eq('0') - end - - it "sets the X-Frame-Options header" do - get :index - expect(response.headers['X-Frame-Options']).to eq('DENY') - end - - it "does not set CSP header" do - get :index - expect(response.headers['Content-Security-Policy-Report-Only']).to eq(nil) - end - - #mock ssl - it "sets the Strict-Transport-Security header" do - request.env['HTTPS'] = 'on' - get :index - expect(response.headers['Strict-Transport-Security']).to eq("max-age=315576000") - end - - it "sets the X-Download-Options header" do - get :index - expect(response.headers['X-Download-Options']).to eq('noopen') - end - - it "sets the X-Content-Type-Options header" do - get :index - expect(response.headers['X-Content-Type-Options']).to eq("nosniff") - end - - it "sets the X-Permitted-Cross-Domain-Policies" do - get :index - expect(response.headers['X-Permitted-Cross-Domain-Policies']).to eq("none") - end - - context "using IE" do - it "sets the X-Content-Type-Options header" do - request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0" - get :index - expect(response.headers['X-Content-Type-Options']).to eq("nosniff") - end - end - end -end diff --git a/fixtures/rails_4_1_8/spec/spec_helper.rb b/fixtures/rails_4_1_8/spec/spec_helper.rb deleted file mode 100644 index 569d69cb..00000000 --- a/fixtures/rails_4_1_8/spec/spec_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rubygems' - -#uncomment the following line to use spork with the debugger -#require 'spork/ext/ruby-debug' - -# Spork.prefork do - # Loading more in this block will cause your tests to run faster. However, - # if you change any configuration or code from libraries loaded here, you'll - # need to restart spork for it take effect. -# This file is copied to spec/ when you run 'rails generate rspec:install' - ENV["RAILS_ENV"] ||= 'test' - require File.expand_path("../../config/environment", __FILE__) - require 'rspec/rails' -# end - diff --git a/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep b/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep b/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep deleted file mode 100644 index e69de29b..00000000 From 2b41aca1549e5f13331aff6e3892e3756d34fbae Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 20 Oct 2015 10:02:24 -1000 Subject: [PATCH 073/636] fix regression with mutation of global state --- .../headers/content_security_policy.rb | 21 +++++++++---------- .../headers/content_security_policy_spec.rb | 8 +++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 73e81f09..96305bdd 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -133,15 +133,6 @@ def initialize(config=nil, options={}) @ua = options[:ua] @ssl_request = !!options.delete(:ssl) @request_uri = options.delete(:request_uri) - @http_additions = config.delete(:http_additions) - @disable_img_src_data_uri = !!config.delete(:disable_img_src_data_uri) - @tag_report_uri = !!config.delete(:tag_report_uri) - @script_hashes = config.delete(:script_hashes) || [] - @app_name = config.delete(:app_name) - @app_name = @app_name.call(@controller) if @app_name.respond_to?(:call) - @enforce = config.delete(:enforce) - @enforce = @enforce.call(@controller) if @enforce.respond_to?(:call) - @enforce = !!@enforce # Config values can be string, array, or lamdba values @config = config.inject({}) do |hash, (key, value)| @@ -153,14 +144,22 @@ def initialize(config=nil, options={}) translate_dir_value(val) end.flatten.uniq end - elsif key != :script_hash_middleware - raise ArgumentError.new("Unknown directive supplied: #{key}") end hash[key] = config_val hash end + @http_additions = @config.delete(:http_additions) + @disable_img_src_data_uri = !!@config.delete(:disable_img_src_data_uri) + @tag_report_uri = !!@config.delete(:tag_report_uri) + @script_hashes = @config.delete(:script_hashes) || [] + @app_name = @config.delete(:app_name) + @app_name = @app_name.call(@controller) if @app_name.respond_to?(:call) + @enforce = @config.delete(:enforce) + @enforce = @enforce.call(@controller) if @enforce.respond_to?(:call) + @enforce = !!@enforce + # normalize and tag the report-uri if @config[:report_uri] @config[:report_uri] = @config[:report_uri].map do |report_uri| diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index d8fb6150..69b3b6ef 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -142,6 +142,14 @@ def request_for user_agent, request_uri=nil, options={:ssl => false} end describe "#value" do + it "does not mutate shared state" do + opts = default_opts.merge(enforce: true) + policy = ContentSecurityPolicy.new(opts, :request => request_for(CHROME)) + expect(policy.name).to eq("Content-Security-Policy") + policy = ContentSecurityPolicy.new(opts, :request => request_for(CHROME)) + expect(policy.name).to eq("Content-Security-Policy") + end + context "browser sniffing" do let(:complex_opts) do ALL_DIRECTIVES.inject({}) { |memo, directive| memo[directive] = "'self'"; memo }.merge(:block_all_mixed_content => '') From 7a4271051cce8a8c39ac3d2a58688966d656f081 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 20 Oct 2015 10:17:38 -1000 Subject: [PATCH 074/636] version bump for regression --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 5251d57f..a97dfb5c 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.4.1" + VERSION = "2.4.2" end From 48b048c6472a324c77297751f4e5a5abf2d42b27 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 23 Oct 2015 07:57:22 -1000 Subject: [PATCH 075/636] Cache UserAgentParser instance Thanks to @igrep for pointing this out. Fixes #187 --- lib/secure_headers/headers/content_security_policy.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 96305bdd..66bfd63e 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -11,6 +11,7 @@ module Constants DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:" HEADER_NAME = "Content-Security-Policy" ENV_KEY = 'secure_headers.content_security_policy' + USER_AGENT_PARSER = UserAgentParser::Parser.new DIRECTIVES_1_0 = [ :default_src, @@ -304,7 +305,7 @@ def strip_unsupported_directives end def supported_directives - @supported_directives ||= case UserAgentParser.parse(@ua).family + @supported_directives ||= case USER_AGENT_PARSER.parse(@ua).family when "Chrome" CHROME_DIRECTIVES when "Safari" @@ -317,7 +318,7 @@ def supported_directives end def supports_nonces? - parsed_ua = UserAgentParser.parse(@ua) + parsed_ua = USER_AGENT_PARSER.parse(@ua) ["Chrome", "Opera", "Firefox"].include?(parsed_ua.family) end end From 35536c0b6486b20f671148f52a9bc4748141d364 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 23 Oct 2015 08:31:29 -1000 Subject: [PATCH 076/636] version bump for performance regression --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index a97dfb5c..d6148c98 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.4.2" + VERSION = "2.4.3" end From 32bb3f51e8d877baf1e3e4461663ae0b7b1347c0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 6 Oct 2015 14:30:16 -1000 Subject: [PATCH 077/636] Major rewrite: Configure a global default and named overrides Use helper methods to set/modify configurations at runtime Set the headers in middleware based on the configuration saved to request.env Configuration changes: All headers require string values except for CSP and HPKP CSP directives must be arrays of strings, no more support for space-delimited strings or procs --- .rspec | 1 + .travis.yml | 2 + Gemfile | 19 +- Guardfile | 12 + README.md | 466 +++++--------- Rakefile | 4 +- lib/secure_headers.rb | 396 +++++++----- lib/secure_headers/configuration.rb | 189 ++++++ lib/secure_headers/hash_helper.rb | 7 - lib/secure_headers/header.rb | 5 - .../headers/content_security_policy.rb | 594 ++++++++++-------- .../script_hash_middleware.rb | 22 - lib/secure_headers/headers/public_key_pins.rb | 101 ++- .../headers/strict_transport_security.rb | 69 +- .../headers/x_content_type_options.rb | 50 +- .../headers/x_download_options.rb | 50 +- lib/secure_headers/headers/x_frame_options.rb | 57 +- .../x_permitted_cross_domain_policies.rb | 52 +- .../headers/x_xss_protection.rb | 64 +- lib/secure_headers/middleware.rb | 15 + lib/secure_headers/padrino.rb | 3 +- lib/secure_headers/railtie.rb | 15 +- lib/secure_headers/version.rb | 3 - lib/secure_headers/view_helper.rb | 70 +-- lib/tasks/tasks.rake | 48 -- secure_headers.gemspec | 18 +- spec/lib/secure_headers/configuration_spec.rb | 80 +++ .../script_hash_middleware_spec.rb | 46 -- .../headers/content_security_policy_spec.rb | 379 ++++------- .../headers/public_key_pins_spec.rb | 34 +- .../headers/strict_transport_security_spec.rb | 54 +- .../headers/x_content_type_options_spec.rb | 31 +- .../headers/x_download_options_spec.rb | 30 +- .../headers/x_frame_options_spec.rb | 32 +- .../x_permitted_cross_domain_policies_spec.rb | 61 +- .../headers/x_xss_protection_spec.rb | 52 +- spec/lib/secure_headers/middleware_spec.rb | 40 ++ spec/lib/secure_headers_spec.rb | 524 ++++++--------- spec/spec_helper.rb | 60 +- 39 files changed, 1732 insertions(+), 2023 deletions(-) create mode 100644 .rspec create mode 100644 Guardfile create mode 100644 lib/secure_headers/configuration.rb delete mode 100644 lib/secure_headers/hash_helper.rb delete mode 100644 lib/secure_headers/header.rb delete mode 100644 lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb create mode 100644 lib/secure_headers/middleware.rb delete mode 100644 lib/secure_headers/version.rb delete mode 100644 lib/tasks/tasks.rake create mode 100644 spec/lib/secure_headers/configuration_spec.rb delete mode 100644 spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb create mode 100644 spec/lib/secure_headers/middleware_spec.rb diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..fb4b5a2a --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--order rand diff --git a/.travis.yml b/.travis.yml index 6246523c..eb27c43f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,7 @@ rvm: - "1.9.3" - "jruby-19mode" +before_install: gem update bundler + sudo: false cache: bundler diff --git a/Gemfile b/Gemfile index 93a2a150..b227a920 100644 --- a/Gemfile +++ b/Gemfile @@ -1,16 +1,13 @@ -source 'https://rubygems.org' +source "https://rubygems.org" gemspec group :test do - gem 'test-unit', '~> 3.0' - gem 'rails', '3.2.22' - gem 'sqlite3', :platforms => [:ruby, :mswin, :mingw] - gem 'jdbc-sqlite3', :platforms => [:jruby] - gem 'rspec-rails', '>= 3.1' - gem 'rspec', '>= 3.1' - gem 'growl' - gem 'rb-fsevent' - gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21] - gem 'i18n', '< 0.7.0', :platforms => [:ruby_18] + gem "pry-nav" + gem "rack" + gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] + gem "rspec", ">= 3.1" + gem "growl" + gem "rb-fsevent" + gem "coveralls", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] end diff --git a/Guardfile b/Guardfile new file mode 100644 index 00000000..f3d4ebd9 --- /dev/null +++ b/Guardfile @@ -0,0 +1,12 @@ +guard :rspec, cmd: "bundle exec rspec", all_on_start: true, all_after_pass: true do + require "guard/rspec/dsl" + dsl = Guard::RSpec::Dsl.new(self) + + # RSpec files + rspec = dsl.rspec + watch(rspec.spec_helper) { rspec.spec_dir } + watch(rspec.spec_support) { rspec.spec_dir } + watch(rspec.spec_files) + + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } +end diff --git a/README.md b/README.md index f4f5d0c5..868a12af 100644 --- a/README.md +++ b/README.md @@ -10,243 +10,197 @@ The gem will automatically apply several headers that are related to security. - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinnning Specification](https://tools.ietf.org/html/rfc7469) -## Usage - -- `ensure_security_headers` in a controller will set security-related headers automatically based on the configuration below. - -### Disabling - -Use the standard `skip_before_filter :filter_name, options` mechanism. e.g. `skip_before_filter :set_csp_header, :only => :tinymce_page` - -The following methods are going to be called, unless they are provided in a `skip_before_filter` block. - -* `:set_csp_header` -* `:set_hsts_header` -* `:set_hpkp_header` -* `:set_x_frame_options_header` -* `:set_x_xss_protection_header` -* `:set_x_content_type_options_header` -* `:set_x_download_options_header` -* `:set_x_permitted_cross_domain_policies_header` +`secure_headers` is a library with a global config, per request overrides, and rack milddleware that enables you customize your application settings. ## Configuration -**Place the following in an initializer (recommended):** +**Place the following in an initializer** if you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. -**NOTE: All CSP config values accept procs for one way of dynamically setting values** +All `nil` values will fallback to their default value. `SecureHeaders::OPT_OUT` will disable the header entirely. ```ruby -::SecureHeaders::Configuration.configure do |config| - config.hsts = {:max_age => 20.years.to_i, :include_subdomains => true} - config.x_frame_options = 'DENY' +SecureHeaders::Configuration.default do |config| + config.hsts = 20.years.to_i.to_s + config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" - config.x_xss_protection = {:value => 1, :mode => 'block'} - config.x_download_options = 'noopen' - config.x_permitted_cross_domain_policies = 'none' + config.x_xss_protection = "1; mode=block" + config.x_download_options = "noopen" + config.x_permitted_cross_domain_policies = "none" config.csp = { - :default_src => "https: 'self'", - :enforce => proc {|controller| controller.my_feature_flag_api.enabled? }, - :frame_src => "https: http:.twimg.com http://itunes.apple.com", - :img_src => "https:", - :connect_src => "wws:" - :font_src => "'self' data:", - :frame_src => "'self'", - :img_src => "mycdn.com data:", - :media_src => "utoob.com", - :object_src => "'self'", - :script_src => "'self'", - :style_src => "'unsafe-inline'", - :base_uri => "'self'", - :child_src => "'self'", - :form_action => "'self' github.com", - :frame_ancestors => "'none'", - :plugin_types => 'application/x-shockwave-flash', - :block_all_mixed_content => '' # see [http://www.w3.org/TR/mixed-content/]() - :report_uri => '//example.com/uri-directive' + :default_src => %w(https: 'self'), + :report_only => false, + :frame_src => %w(https: http:.twimg.com http://itunes.apple.com), + :connect_src => %w(wws:), + :font_src => %w('self' data:), + :frame_src => %w('self'), + :img_src => %w(mycdn.com data:), + :media_src => %w(utoob.com), + :object_src => %w('self'), + :script_src => %w('self'), + :style_src => %w('unsafe-inline'), + :base_uri => %w('self'), + :child_src => %w('self'), + :form_action => %w('self' github.com), + :frame_ancestors => %w('none'), + :plugin_types => %w(application/x-shockwave-flash), + :block_all_mixed_content => true # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) + :report_uri => %w(https://example.com/uri-directive) } config.hpkp = { :max_age => 60.days.to_i, :include_subdomains => true, - :report_uri => '//example.com/uri-directive', + :report_uri => "https//example.com/uri-directive", :pins => [ - {:sha256 => 'abc'}, - {:sha256 => '123'} + {:sha256 => "abc"}, + {:sha256 => "123"} ] } end - -# and then include this in application_controller.rb -class ApplicationController < ActionController::Base - ensure_security_headers -end ``` -Or do the config as a parameter to `ensure_security_headers` +### rails 2 + +`secure_headers` has a `railtie` that should automatically include the middleware. ```ruby -ensure_security_headers( - :hsts => {:include_subdomains => true, :max_age => 20.years.to_i}, - :x_frame_options => 'DENY', - :csp => false -) +use SecureHeaders::Middleware ``` -## Per-action configuration +## Default values + +All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). -Sometimes you need to override your content security policy for a given endpoint. Rather than applying the exception globally, you have a few options: +## Named overrides -1. Use procs as config values as mentioned above. -1. Specifying `ensure_security_headers csp: ::SecureHeaders::Configuration.csp.merge(script_src: shadyhost.com)` in a descendent controller will override the settings for that controller only. -1. Override the `secure_header_options_for` class instance method. e.g. +Named overrides serve two purposes: + +* To be able to refer to a configuration by simple name. +* By precomputing the headers for a named configuration, the headers generated once and reused over every request. + +To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override. ```ruby -class SomethingController < ApplicationController - def wumbus - # gets style-src override +class ApplicationController < ActionController::Base + SecureHeaders::Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w(example.org) + } end - def diffendoofer - # does not get style-src override + # override default configuration + SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config| + config.csp[:script_src] << "otherdomain.com" end - def secure_header_options_for(header, options) - options = super - if params[:action] == "wumbus" - if header == :csp - options.merge(style_src: "'self'") - end - else - options - end + # overrides the :script_from_otherdomain_com configuration + SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config| + config.csp[:script_src] << "evenanotherdomain.com" end end -``` - -## Options for ensure\_security\_headers -**To disable any of these headers, supply a value of false (e.g. :hsts => false), supplying nil will set the default value** - -Each header configuration can take a hash, or a string, or both. If a string -is provided, that value is inserted verbatim. If a hash is supplied, a -header will be constructed using the supplied options. - -### The Easy Headers - -This configuration will likely work for most applications without modification. +class MyController < ApplicationController + def index + # Produces default-src 'self'; script-src example.org otherdomain.org + use_secure_headers_override(:script_from_otherdomain_com) + end -```ruby -:hsts => {:max_age => 631138519, :include_subdomains => false} -:x_frame_options => {:value => 'SAMEORIGIN'} -:x_xss_protection => {:value => 1, :mode => 'block'} # set the :mode option to false to use "warning only" mode -:x_content_type_options => {:value => 'nosniff'} -:x_download_options => {:value => 'noopen'} -:x_permitted_cross_domain_policies => {:value => 'none'} + def show + # Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com + use_secure_headers_override(:another_config) + end +end ``` -### Content Security Policy (CSP) +By default, a noop configuration is provided. No headers will be set when this default override is used. ```ruby -:csp => { - :enforce => false, # sets header to report-only, by default - # default_src is required! - :default_src => nil, # sets the default-src/allow+options directives - - # Where reports are sent. Use protocol relative URLs if you are posting to the same domain (TLD+1). Use paths if you are posting to the application serving the header - :report_uri => '//mysite.example.com', - - # these directives all take 'none', 'self', or a globbed pattern - :img_src => nil, - :frame_src => nil, - :connect_src => nil, - :font_src => nil, - :media_src => nil, - :object_src => nil, - :style_src => nil, - :script_src => nil, - - # http additions will be appended to the various directives when - # over http, relaxing the policy - # e.g. - # :csp => { - # :img_src => 'https:', - # :http_additions => {:img_src => 'http'} - # } - # would produce the directive: "img-src https: http:;" - # when over http, ignored for https requests - :http_additions => {} -} +class MyController < ApplicationController + def index + SecureHeaders::opt_out_of_all_protection(request) + # or + use_secure_headers_override(SecureHeaders::Configuration::NOOP_CONFIGURATION) + end +end ``` -### Example CSP header config +## Per-action configuration +You can override the settings for a given action by producing a temporary override. This approach is not recommended because the header values will be computed per request. ```ruby -# most basic example -:csp => { - :default_src => "https: 'unsafe-inline' 'unsafe-eval'", - :report_uri => '/uri-directive' -} - -> "default-src 'unsafe-inline' 'unsafe-eval' https:; report-uri /uri-directive;" +# Given a config of: +::SecureHeaders::Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self') + } + end -# turn off inline scripting/eval -:csp => { - :default_src => 'https:', - :report_uri => '/uri-directive' -} +class MyController < ApplicationController + def index + # Append value to the source list, override 'none' values + # Produces: default-src 'self'; script-src 'self' s3.amazaonaws.com; object-src 'self' youtube.com + append_content_security_policy_directives(script_src: %w(s3.amazaonaws.com), object_src: %w('self' youtube.com)) -> "default-src https:; report-uri /uri-directive;" + # Overrides the previously set source list, override 'none' values + # Produces: default-src 'self'; script-src s3.amazaonaws.com; object-src 'self' + override_content_security_policy_directive(script_src: "s3.amazaonaws.com", object_src: %w('self')) -# Auction site wants to allow images from anywhere, plugin content from a list of trusted media providers (including a content distribution network), and scripts only from its server hosting sanitized JavaScript -:csp => { - :default_src => "'self'", - :img_src => '*', - :object_src => ['media1.com', 'media2.com', '*.cdn.com'], - # alternatively (NOT csv) :object_src => 'media1.com media2.com *.cdn.com' - :script_src => 'trustedscripts.example.com' -} -"default-src 'self'; img-src *; object-src media1.com media2.com *.cdn.com; script-src trustedscripts.example.com;" + # Global settings default to "sameorigin" + override_x_frame_options("DENY") + end ``` -### Tagging Requests +The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object. +* `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration. +* `override_content_security_policy_directive(hash)`: merges the hash into the app-wide configuration, overwriting any previous config +* `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value` -It's often valuable to send extra information in the report uri that is not available in the reports themselves. Namely, "was the policy enforced" and "where did the report come from" +## Appending / overriding Content Security Policy -```ruby -{ - :tag_report_uri => true, - :enforce => true, - :app_name => 'twitter', - :report_uri => 'csp_reports' -} -``` +When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`. -Results in -``` -report-uri csp_reports?enforce=true&app_name=twitter -``` +#### Append to the policy with a directive other than `default_src` -### CSP Level 2 features +The value of `default_src` is joined with the addition. Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate: -*NOTE: Currently, only erb is supported. Mustache support isn't far off. Hash sources are valid for inline style blocks but are not yet supported by secure_headers.* +```ruby +::SecureHeaders::Configuration.configure do |config| + config.csp = { + default_src: %w('self') + } + end + ``` -#### Nonce +Code | Result +------------- | ------------- +`append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com` +`override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com` -script/style-nonce can be used to whitelist inline content. To do this, add "nonce" to your script/style-src configuration, then set the nonce attributes on the various tags. +#### Appending to an opted-out Policy -Setting a nonce will also set 'unsafe-inline' for browsers that don't support nonces for backwards compatibility. 'unsafe-inline' is ignored if a nonce is present in a directive in compliant browsers. +If your policy is set to `SecureHeaders::OPT_OUT`, you will be appending to the default policy (`default-src https:`). This behaves the same way as the above example using the default configuration and will set the header based on the result. ```ruby -:csp => { - :default_src => "'self'", - :script_src => "'self' nonce" -} -``` +::SecureHeaders::Configuration.configure do |config| + config.csp = SecureHeaders::OPT_OUT + end + ``` + +Code | Result +------------- | ------------- +`append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src https:; script-src https: mycdn.com` +`override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src https:; script-src mycdn.com` + +#### Nonce -> content-security-policy: default-src 'self'; script-src 'self' 'nonce-abc123' 'unsafe-inline' +script/style-nonce can be used to whitelist inline content. To do this, call the SecureHeaders::content_security_policy_nonce then set the nonce attributes on the various tags. + +Setting a nonce will also set 'unsafe-inline' for browsers that don't support nonces for backwards compatibility. 'unsafe-inline' is ignored if a nonce is present in a directive in compliant browsers. ```erb - @@ -258,7 +212,9 @@ Setting a nonce will also set 'unsafe-inline' for browsers that don't support no console.log("won't execute, not whitelisted") ``` + You can use a view helper to automatically add nonces to script tags: + ```erb <%= nonced_javascript_tag do %> console.log("nonced!") @@ -276,86 +232,13 @@ console.log("nonced!") #### Hash -setting hash source values will also set 'unsafe-inline' for browsers that don't support hash sources for backwards compatibility. 'unsafe-inline' is ignored if a hash is present in a directive in compliant browsers. - -Hash source support works by taking the hash value of the contents of an inline script block and adding the hash "fingerprint" to the CSP header. - -If you only have a few hashes, you can hardcode them for the entire app: - -```ruby - config.csp = { - :default_src => "https:", - :script_src => "'self'" - :script_hashes => ['sha1-abc', 'sha1-qwe'] - } -``` - -The following will work as well, but may not be as clear: - -```ruby - config.csp = { - :default_src => "https:", - :script_src => "'self' 'sha1-qwe'" - } -``` - -If you find you have many hashes or the content of the script tags change frequently, you can apply these hashes in a more intelligent way. This method expects config/script_hashes.yml to contain a map of templates => [hashes]. When the individual templates, layouts, or partials are rendered the hash values for the script tags in those templates will be automatically added to the header. *Currently, only erb layouts are supported.* This requires the use of middleware: - -```ruby -# config.ru -require 'secure_headers/headers/content_security_policy/script_hash_middleware' -use ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware -``` - -```ruby - config.csp = { - :default_src => "https:", - :script_src => "'self'", - :script_hash_middleware => true - } -``` - -Hashes are stored in a yaml file with a mapping of Filename => [list of hashes] in config/script_hashes.yml. You can automatically populate this file by running the following rake task: - -```$ bundle exec rake secure_headers:generate_hashes``` - -Which will generate something like: - -```yaml -# config/script_hashes.yml -app/views/layouts/application.html.erb: -- sha256-l8OLjZqYRnKilpdE0VosRMvhdYArjXT4NZaK2p7QVvs= -app/templates/articles/edit.html.erb: -- sha256-+7mij1/uCwtCQRWrof2NmOln5qX+5WdVwTLMpi8nuoA= -- sha256-Ny4TRIhhFpnYnSeKC274P6bfAz4TOkezLabavIAU4dA= -- sha256-I5e58Gqbu4WpO9dck18QxO7aYOHKrELIi70it4jIPi0= -- sha256-Po4LMynwnAJHxiTp3DQaQ3YDBj3paN/xrDoKl4OyxY4= -``` - -In this example, if we visit /articles/edit/[id], the above hashes will automatically be added to the CSP header's -script-src value! - -You can use plain "script" tags or you can use a built-in helper: - -```erb -<%= hashed_javascript_tag do %> -console.log("hashed automatically!") -<% end %> -``` - -By using the helper, hash values will be computed dynamically in development/test environments. If a dynamically computed hash value does not match what is expected to be found in config/script_hashes.yml a warning message will be printed to the console. If you want to raise exceptions instead, use: - -```erb -<%= hashed_javascript_tag(raise_error_on_unrecognized_hash = true) do %> -console.log("will raise an exception if not in script_hashes.yml!") -<% end %> -``` +The hash feature has been removed, for now. ### Public Key Pins Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work. -``` +```ruby config.hpkp = { max_age: 60.days.to_i, # max_age is a required parameter include_subdomains: true, # whether or not to apply pins to subdomains @@ -364,7 +247,7 @@ config.hpkp = { {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} ], - enforce: true, # defaults to false (report-only mode) + report_only: true, # defaults to false (report-only mode) report_uri: '//example.com/uri-directive', app_name: 'example', tag_report_uri: true @@ -381,28 +264,17 @@ require 'sinatra' require 'haml' require 'secure_headers' -::SecureHeaders::Configuration.configure do |config| - config.hsts = {:max_age => 99, :include_subdomains => true} - config.x_frame_options = 'DENY' - config.x_content_type_options = "nosniff" - config.x_xss_protection = {:value => 1, :mode => false} - config.x_download_options = 'noopen' - config.x_permitted_cross_domain_policies = 'none' - config.csp = { - :default_src => "https: inline eval", - :report_uri => '//example.com/uri-directive', - :img_src => "https: data:", - :frame_src => "https: http:.twimg.com http://itunes.apple.com" - } - config.hpkp = false +use SecureHeaders::Middleware + +SecureHeaders::Configuration.configure do |config| + ... end class Donkey < Sinatra::Application - include SecureHeaders set :root, APP_ROOT get '/' do - set_csp_header + SecureHeaders.override_x_frame_options(SecureHeaders::OPT_OUT) haml :index end end @@ -421,6 +293,7 @@ In your `Gemfile`: then in your `app.rb` file you can: ```ruby +Padrino.use(SecureHeaders::Middleware) require 'secure_headers/padrino' module Web @@ -428,7 +301,6 @@ module Web register SecureHeaders::Padrino get '/' do - set_csp_header render 'index' end end @@ -439,48 +311,8 @@ and in `config/boot.rb`: ```ruby def before_load - ::SecureHeaders::Configuration.configure do |config| - config.hsts = {:max_age => 99, :include_subdomains => true} - config.x_frame_options = 'DENY' - config.x_content_type_options = "nosniff" - config.x_xss_protection = {:value => '1', :mode => false} - config.x_download_options = 'noopen' - config.x_permitted_cross_domain_policies = 'none' - config.csp = { - :default_src => "https: inline eval", - :report_uri => '//example.com/uri-directive', - :img_src => "https: data:", - :frame_src => "https: http:.twimg.com http://itunes.apple.com" - } - end -end -``` - -### Using in rack middleware - -The `SecureHeaders::header_hash` generates a hash of all header values, which is useful for merging with rack middleware values. - -```ruby -class MySecureHeaders - include SecureHeaders - def initialize(app) - @app = app - end - - def call(env) - status, headers, response = @app.call(env) - security_headers = if override? - SecureHeaders::header_hash(:csp => false) # uses global config, but overrides CSP config - else - SecureHeaders::header_hash # uses global config - end - [status, headers.merge(security_headers), [response.body]] - end -end - -module Testapp - class Application < Rails::Application - config.middleware.use MySecureHeaders + SecureHeaders::Configuration.configure do |config| + ... end end ``` @@ -495,18 +327,6 @@ end * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) * Go - [secureheader](https://github.com/kr/secureheader) -## Authors - -* Neil Matatall [@ndm](https://twitter.com/ndm) - primary author. -* Nicholas Green [@nickgreen](https://twitter.com/nickgreen) - code contributions, main reviewer. - -## Acknowledgements - -* Justin Collins [@presidentbeef](https://twitter.com/presidentbeef) & Jim O'Leary [@jimio](https://twitter.com/jimio) for reviews. -* Ian Melven [@imelven](https://twitter.com/imelven) - Discussions/info about CSP in general, made us aware of the [userCSP](https://addons.mozilla.org/en-US/firefox/addon/newusercspdesign/) Mozilla extension. -* Sumit Shah [@omnidactyl](https://twitter.com/omnidactyl) - For being an eager guinea pig. -* Chris Aniszczyk [@cra](https://twitter.com/cra) - For running an awesome open source program at Twitter. - ## License Copyright 2013-2014 Twitter, Inc and other contributors. diff --git a/Rakefile b/Rakefile index 32612c79..bec88c68 100644 --- a/Rakefile +++ b/Rakefile @@ -7,10 +7,10 @@ require 'net/https' desc "Run RSpec" RSpec::Core::RakeTask.new do |t| t.verbose = false - t.rspec_opts = "--format progress" + t.rspec_opts = "--format progress" end -task :default => :spec +task default: :spec begin require 'rdoc/task' diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 05cf1922..fd4458d9 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -1,5 +1,4 @@ -require "secure_headers/version" -require "secure_headers/header" +require "secure_headers/configuration" require "secure_headers/headers/public_key_pins" require "secure_headers/headers/content_security_policy" require "secure_headers/headers/x_frame_options" @@ -8,218 +7,287 @@ require "secure_headers/headers/x_content_type_options" require "secure_headers/headers/x_download_options" require "secure_headers/headers/x_permitted_cross_domain_policies" +require "secure_headers/middleware" require "secure_headers/railtie" -require "secure_headers/hash_helper" require "secure_headers/view_helper" +require "useragent" +# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT +# or ":optout_of_protection" as a config value to disable a given header module SecureHeaders - SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml' - HASHES_ENV_KEY = 'secure_headers.script_hashes' + OPT_OUT = :opt_out_of_protection + SCRIPT_HASH_CONFIG_FILE = "config/script_hashes.yml".freeze + SECURE_HEADERS_CONFIG = "secure_headers".freeze + NONCE_KEY = "content_security_policy_nonce".freeze + HTTPS = "https".freeze + CSP = ContentSecurityPolicy ALL_HEADER_CLASSES = [ - SecureHeaders::ContentSecurityPolicy, - SecureHeaders::StrictTransportSecurity, - SecureHeaders::PublicKeyPins, - SecureHeaders::XContentTypeOptions, - SecureHeaders::XDownloadOptions, - SecureHeaders::XFrameOptions, - SecureHeaders::XPermittedCrossDomainPolicies, - SecureHeaders::XXssProtection - ] - - module Configuration - class << self - attr_accessor :hsts, :x_frame_options, :x_content_type_options, - :x_xss_protection, :csp, :x_download_options, :script_hashes, - :x_permitted_cross_domain_policies, :hpkp - - def configure &block - instance_eval &block - if File.exists?(SCRIPT_HASH_CONFIG_FILE) - ::SecureHeaders::Configuration.script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE)) - end - end - end - end + ContentSecurityPolicy, + StrictTransportSecurity, + PublicKeyPins, + XContentTypeOptions, + XDownloadOptions, + XFrameOptions, + XPermittedCrossDomainPolicies, + XXssProtection + ].freeze + + ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP]).freeze + + # Headers set on http requests (excludes STS and HPKP) + HTTP_HEADER_CLASSES = + (ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze class << self - def append_features(base) - base.module_eval do - extend ClassMethods - include InstanceMethods - end + # Public: opts out of setting a given header by creating a temporary config + # and setting the given headers config to OPT_OUT. + def opt_out_of_header(request, header_key) + config = config_for(request).dup + config.send("#{header_key}=", OPT_OUT) + override_secure_headers_request_config(request, config) end - def header_hash(options = nil) - ALL_HEADER_CLASSES.inject({}) do |memo, klass| - config = if options.is_a?(Hash) && options[klass::Constants::CONFIG_KEY] - options[klass::Constants::CONFIG_KEY] - else - ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY) - end + # Public: opts out of setting all headers by telling secure_headers to use + # the NOOP configuration. + def opt_out_of_all_protection(request) + use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION) + end - unless klass == SecureHeaders::PublicKeyPins && !config.is_a?(Hash) - header = get_a_header(klass::Constants::CONFIG_KEY, klass, config) - memo[header.name] = header.value - end - memo + # Public: override a given set of directives for the current request. If a + # value already exists for a given directive, it will be overridden. + # + # additions - a hash containing directives. e.g. + # :script_src => %w(another-host.com) + def override_content_security_policy_directives(request, additions) + config = config_for(request).dup + if config.csp == OPT_OUT + config.csp = {} end + config.csp.merge!(additions) + override_secure_headers_request_config(request, config) end - def get_a_header(name, klass, options) - return if options == false - klass.new(options) + # Public: appends source values to the current configuration. If no value + # is set for a given directive, the value will be merged with the default-src + # value. If a value exists for the given directive, the values will be combined. + # + # additions - a hash containing directives. e.g. + # :script_src => %w(another-host.com) + def append_content_security_policy_directives(request, additions) + config = config_for(request).dup + config.csp = CSP.combine_policies(config.csp, additions) + override_secure_headers_request_config(request, config) + end + + # Public: override X-Frame-Options settings for this request. + # + # value - deny, sameorigin, or allowall + # + # Returns the current config + def override_x_frame_options(request, value) + default_config = config_for(request).dup + default_config.x_frame_options = value + override_secure_headers_request_config(request, default_config) end - end - module ClassMethods - attr_writer :secure_headers_options - def secure_headers_options - if @secure_headers_options - @secure_headers_options - elsif superclass.respond_to?(:secure_headers_options) # stop at application_controller - superclass.secure_headers_options + # Public: Builds the hash of headers that should be applied base on the + # request. + # + # StrictTransportSecurity and PublicKeyPins are not applied to http requests. + # See #config_for to determine which config is used for a given request. + # + # Returns a hash of header names => header values. The value + # returned is meant to be merged into the header value from `@app.call(env)` + # in Rack middleware. + def header_hash_for(request) + config = config_for(request) + + headers = if cached_headers = config.cached_headers + use_cached_headers(cached_headers, request) else - {} + build_headers(config, request) end + + headers end - def ensure_security_headers options = {} - if RUBY_VERSION == "1.8.7" - warn "[DEPRECATION] secure_headers ruby 1.8.7 support will dropped in the next release" + # Public: specify which named override will be used for this request. + # Raises an argument error if no named override exists. + # + # name - the name of the previously configured override. + def use_secure_headers_override(request, name) + if config = Configuration.get(name) + override_secure_headers_request_config(request, config) + else + raise ArgumentError.new("no override by the name of #{name} has been configured") end - self.secure_headers_options = options - before_filter :prep_script_hash - before_filter :set_hsts_header - before_filter :set_hpkp_header - before_filter :set_x_frame_options_header - before_filter :set_csp_header - before_filter :set_x_xss_protection_header - before_filter :set_x_content_type_options_header - before_filter :set_x_download_options_header - before_filter :set_x_permitted_cross_domain_policies_header end - end - module InstanceMethods - def set_security_headers(options = self.class.secure_headers_options) - set_csp_header(request, options[:csp]) - set_hsts_header(options[:hsts]) - set_hpkp_header(options[:hpkp]) - set_x_frame_options_header(options[:x_frame_options]) - set_x_xss_protection_header(options[:x_xss_protection]) - set_x_content_type_options_header(options[:x_content_type_options]) - set_x_download_options_header(options[:x_download_options]) - set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies]) - end - - # set_csp_header - uses the request accessor and SecureHeader::Configuration settings - # set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings - # set_csp_header(+Hash+) - uses the request accessor and options from parameters - # set_csp_header(+Rack::Request+, +Hash+) - def set_csp_header(req = nil, config=nil) - if req.is_a?(Hash) || req.is_a?(FalseClass) - config = req - end + # Public: gets or creates a nonce for CSP. + # + # The nonce will be added to script_src + # + # Returns the nonce + def content_security_policy_script_nonce(request) + content_security_policy_nonce(request, CSP::SCRIPT_SRC) + end - config = self.class.secure_headers_options[:csp] if config.nil? - config = secure_header_options_for :csp, config + # Public: gets or creates a nonce for CSP. + # + # The nonce will be added to script_src + # + # Returns the nonce + def content_security_policy_style_nonce(request) + content_security_policy_nonce(request, CSP::STYLE_SRC) + end - return if config == false + private - if config && config[:script_hash_middleware] - ContentSecurityPolicy.add_to_env(request, self, config) - else - csp_header = ContentSecurityPolicy.new(config, :request => request, :controller => self) - set_header(csp_header) - end + # Private: gets or creates a nonce for CSP. + # + # Returns the nonce + def content_security_policy_nonce(request, script_or_style) + request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp + nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce + append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY]) + request.env[NONCE_KEY] end + # Private: retrieve the configuration being used for the current request. + # + # Retrives the default, overridden, or custom configuration for the given + # request. + def secure_headers_request_config(request) + request.env[SECURE_HEADERS_CONFIG] + end - def prep_script_hash - if ::SecureHeaders::Configuration.script_hashes - @script_hashes = ::SecureHeaders::Configuration.script_hashes.dup - ActiveSupport::Notifications.subscribe("render_partial.action_view") do |event_name, start_at, end_at, id, payload| - save_hash_for_later payload - end + # Private: convenience method for specifying which configuration object should + # be used for this request. + # + # Returns the config. + def override_secure_headers_request_config(request, config) + request.env[SECURE_HEADERS_CONFIG] = config + end - ActiveSupport::Notifications.subscribe("render_template.action_view") do |event_name, start_at, end_at, id, payload| - save_hash_for_later payload - end + # Private: determines which headers are applicable to a given request. + # + # Returns a list of classes whose corresponding header values are valid for + # this request. + def header_classes_for(request) + if request.scheme == HTTPS + ALL_HEADER_CLASSES + else + HTTP_HEADER_CLASSES end end - def save_hash_for_later payload - matching_hashes = @script_hashes[payload[:identifier].gsub(Rails.root.to_s + "/", "")] || [] - - if payload[:layout] - # We're assuming an html.erb layout for now. Will need to handle mustache too, just not sure of the best way to do this - layout_hashes = @script_hashes[File.join("app", "views", payload[:layout]) + '.html.erb'] - - matching_hashes << layout_hashes if layout_hashes - end + # Private: do the heavy lifting of converting a configuration object + # to a hash of headers valid for this request. + # + # Returns a hash of header names / values. + def build_headers(config, request) + header_classes_for(request).each_with_object({}) do |klass, hash| + header_config = if config + config.fetch(klass::CONFIG_KEY) + end - if matching_hashes.any? - request.env[HASHES_ENV_KEY] = ((request.env[HASHES_ENV_KEY] || []) << matching_hashes).flatten + header_name, value = if klass == CSP + make_header(klass, header_config, request.user_agent) + else + make_header(klass, header_config) + end + hash[header_name] = value if value end end - def set_x_frame_options_header(options=self.class.secure_headers_options[:x_frame_options]) - set_a_header(:x_frame_options, XFrameOptions, options) + # Private: takes a precomputed hash of headers and returns the Headers + # customized for the request. + # + # Returns a hash of header names / values valid for a given request. + def use_cached_headers(default_headers, request) + header_classes_for(request).each_with_object({}) do |klass, hash| + if default_header = default_headers[klass::CONFIG_KEY] + header_name, value = if klass == CSP + default_csp_header_for_ua(default_header, request) + else + default_header + end + hash[header_name] = value + end + end end - def set_x_content_type_options_header(options=self.class.secure_headers_options[:x_content_type_options]) - set_a_header(:x_content_type_options, XContentTypeOptions, options) + # Private: Retreives the config for a given header type: + # + # Checks to see if there is an override for this request, then + # Checks to see if a named override is used for this request, then + # Falls back to the global config + def config_for(request) + secure_headers_request_config(request) || + Configuration.get(Configuration::DEFAULT_CONFIG) end - def set_x_xss_protection_header(options=self.class.secure_headers_options[:x_xss_protection]) - set_a_header(:x_xss_protection, XXssProtection, options) + # Private: chooses the applicable CSP header for the provided user agent. + # + # headers - a hash of header_config_key => [header_name, header_value] + # + # Returns a CSP [header, value] array + def default_csp_header_for_ua(headers, request) + family = UserAgent.parse(request.user_agent).browser + if CSP::VARIATIONS.key?(family) + headers[family] + else + headers[CSP::OTHER] + end end - def set_hsts_header(options=self.class.secure_headers_options[:hsts]) - return unless request.ssl? - set_a_header(:hsts, StrictTransportSecurity, options) + # Private: optionally build a header with a given configure + # + # klass - corresponding Class for a given header + # config - A string, symbol, or hash config for the header + # user_agent - A string representing the UA (only used for CSP feature sniffing) + # + # Returns a 2 element array [header_name, header_value] or nil if config + # is OPT_OUT + def make_header(klass, header_config, user_agent = nil) + unless header_config == OPT_OUT + if klass == CSP + klass.make_header(header_config, user_agent) + else + klass.make_header(header_config) + end + end end + end - def set_hpkp_header(options=self.class.secure_headers_options[:hpkp]) - return unless request.ssl? - config = secure_header_options_for :hpkp, options - - return if config == false || config.nil? - - hpkp_header = PublicKeyPins.new(config) - set_header(hpkp_header) - end + # These methods are mixed into controllers and delegate to the class method + # with the same name. + def use_secure_headers_override(name) + SecureHeaders.use_secure_headers_override(request, name) + end - def set_x_download_options_header(options=self.class.secure_headers_options[:x_download_options]) - set_a_header(:x_download_options, XDownloadOptions, options) - end + def content_security_policy_script_nonce + SecureHeaders.content_security_policy_script_nonce(request) + end - def set_x_permitted_cross_domain_policies_header(options=self.class.secure_headers_options[:x_permitted_cross_domain_policies]) - set_a_header(:x_permitted_cross_domain_policies, XPermittedCrossDomainPolicies, options) - end + def content_security_policy_style_nonce + SecureHeaders.content_security_policy_style_nonce(request) + end - private + def opt_out_of_header(header_key) + SecureHeaders.opt_out_of_header(request, header_key) + end - # we can't use ||= because I'm overloading false => disable, nil => default - # both of which trigger the conditional assignment - def secure_header_options_for(type, options) - options.nil? ? ::SecureHeaders::Configuration.send(type) : options - end + def append_content_security_policy_directives(additions) + SecureHeaders.append_content_security_policy_directives(request, additions) + end - def set_a_header(name, klass, options=nil) - options = secure_header_options_for(name, options) - return if options == false - set_header(SecureHeaders::get_a_header(name, klass, options)) - end + def override_content_security_policy_directives(additions) + SecureHeaders.override_content_security_policy_directives(request, additions) + end - def set_header(name_or_header, value=nil) - if name_or_header.is_a?(Header) - header = name_or_header - response.headers[header.name] = header.value - else - response.headers[name_or_header] = value - end - end + def override_x_frame_options(value) + SecureHeaders.override_x_frame_options(request, value) end end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb new file mode 100644 index 00000000..be2b0c24 --- /dev/null +++ b/lib/secure_headers/configuration.rb @@ -0,0 +1,189 @@ +module SecureHeaders + class Configuration + DEFAULT_CONFIG = :default + NOOP_CONFIGURATION = "secure_headers_noop_config" + class NotYetConfiguredError < StandardError; end + class << self + # Public: Set the global default configuration. + # + # Optionally supply a block to override the defaults set by this library. + # + # Returns the newly created config. + def default(&block) + config = new(&block) + add_noop_configuration + add_configuration(DEFAULT_CONFIG, config) + end + alias_method :configure, :default + + # Public: create a named configuration that overrides the default config. + # + # name - use an idenfier for the override config. + # base - override another existing config, or override the default config + # if no value is supplied. + # + # Returns: the newly created config + def override(name, base = DEFAULT_CONFIG) + unless get(base) + raise NotYetConfiguredError, "#{base} policy not yet supplied" + end + override = @configurations[base].dup + yield(override) + add_configuration(name, override) + end + + # Public: retrieve a global configuration object + # + # Returns the configuration with a given name or raises a + # NotYetConfiguredError if `default` has not been called. + def get(name = DEFAULT_CONFIG) + if @configurations.nil? + raise NotYetConfiguredError, "Default policy not yet supplied" + end + @configurations[name] + end + + # Public: perform a basic deep dup. The shallow copy provided by dup/clone + # can lead to modifying parent objects. + def deep_copy(config) + config.each_with_object({}) do |(key, value), hash| + hash[key] = if value.is_a?(Array) + value.dup + else + value + end + end + end + + private + + # Private: add a valid configuration to the global set of named configs. + # + # config - the config to store + # name - the lookup value for this config + # + # Raises errors if the config is invalid or if a config named `name` + # already exists. + # + # Returns the config, if valid + def add_configuration(name, config) + config.validate_config! + @configurations ||= {} + config.send(:cache_headers!) + config.freeze + @configurations[name] = config + end + + # Private: Automatically add an "opt-out of everything" override. + # + # Returns the noop config + def add_noop_configuration + noop_config = new do |config| + ALL_HEADER_CLASSES.each do |klass| + config.send("#{klass::CONFIG_KEY}=", OPT_OUT) + end + end + + add_configuration(NOOP_CONFIGURATION, noop_config) + end + end + + attr_accessor :hsts, :x_frame_options, :x_content_type_options, + :x_xss_protection, :csp, :x_download_options, :x_permitted_cross_domain_policies, + :hpkp + attr_reader :cached_headers + + def initialize(&block) + self.hpkp = OPT_OUT + self.csp = self.class.deep_copy(CSP::DEFAULT_CONFIG) + instance_eval &block if block_given? + end + + # Public: copy everything but the cached headers + # + # Returns a deep-dup'd copy of this configuration. + def dup + copy = self.class.new + copy.hsts = hsts + copy.x_frame_options = x_frame_options + copy.x_content_type_options = x_content_type_options + copy.x_xss_protection = x_xss_protection + copy.x_download_options = x_download_options + copy.x_permitted_cross_domain_policies = x_permitted_cross_domain_policies + copy.csp = if csp.is_a?(Hash) + self.class.deep_copy(csp) + else + csp + end + + copy.hpkp = if hpkp.is_a?(Hash) + self.class.deep_copy(hpkp) + else + hpkp + end + copy + end + + # Public: Retrieve a config based on the CONFIG_KEY for a class + # + # Returns the value if available, and returns a dup of any hash values. + def fetch(key) + config = send(key) + config = self.class.deep_copy(config) if config.is_a?(Hash) + config + end + + # Public: validates all configurations values. + # + # Raises various configuration errors if any invalid config is detected. + # + # Returns nothing + def validate_config! + StrictTransportSecurity.validate_config!(hsts) + ContentSecurityPolicy.validate_config!(csp) + XFrameOptions.validate_config!(x_frame_options) + XContentTypeOptions.validate_config!(x_content_type_options) + XXssProtection.validate_config!(x_xss_protection) + XDownloadOptions.validate_config!(x_download_options) + XPermittedCrossDomainPolicies.validate_config!(x_permitted_cross_domain_policies) + PublicKeyPins.validate_config!(hpkp) + end + + # Public: Precompute the header names and values for this configuraiton. + # Ensures that headers generated at configure time, not on demand. + # + # Returns the cached headers + def cache_headers! + # generate defaults for the "easy" headers + headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash| + config = fetch(klass::CONFIG_KEY) + unless config == OPT_OUT + hash[klass::CONFIG_KEY] = klass.make_header(config).freeze + end + end + + generate_csp_headers(headers) + + headers.freeze + @cached_headers = headers + end + + # Private: adds CSP headers for each variation of CSP support. + # + # headers - generated headers are added to this hash namespaced by The + # different variations + # + # Returns nothing + def generate_csp_headers(headers) + unless csp == OPT_OUT + headers[CSP::CONFIG_KEY] = {} + + CSP::VARIATIONS.each do |name, _| + csp_config = fetch(CSP::CONFIG_KEY) + csp = CSP.make_header(csp_config, UserAgent.parse(name)) + headers[CSP::CONFIG_KEY][name] = csp.freeze + end + end + end + end +end diff --git a/lib/secure_headers/hash_helper.rb b/lib/secure_headers/hash_helper.rb deleted file mode 100644 index 19e4044e..00000000 --- a/lib/secure_headers/hash_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module SecureHeaders - module HashHelper - def hash_source(inline_script, digest = :SHA256) - [digest.to_s.downcase, "-", [[Digest.const_get(digest).hexdigest(inline_script)].pack("H*")].pack("m").chomp].join - end - end -end diff --git a/lib/secure_headers/header.rb b/lib/secure_headers/header.rb deleted file mode 100644 index fb06713e..00000000 --- a/lib/secure_headers/header.rb +++ /dev/null @@ -1,5 +0,0 @@ -module SecureHeaders - class Header - - end -end \ No newline at end of file diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 73e81f09..ea458ae5 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -1,325 +1,427 @@ require 'uri' require 'base64' require 'securerandom' -require 'user_agent_parser' require 'json' module SecureHeaders - class ContentSecurityPolicyBuildError < StandardError; end - class ContentSecurityPolicy < Header - module Constants - DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:" - HEADER_NAME = "Content-Security-Policy" - ENV_KEY = 'secure_headers.content_security_policy' - - DIRECTIVES_1_0 = [ - :default_src, - :connect_src, - :font_src, - :frame_src, - :img_src, - :media_src, - :object_src, - :sandbox, - :script_src, - :style_src, - :report_uri - ].freeze - - DIRECTIVES_2_0 = [ - DIRECTIVES_1_0, - :base_uri, - :child_src, - :form_action, - :frame_ancestors, - :plugin_types - ].flatten.freeze - - - # All the directives currently under consideration for CSP level 3. - # https://w3c.github.io/webappsec/specs/CSP2/ - DIRECTIVES_3_0 = [ - DIRECTIVES_2_0, - :manifest_src, - :reflected_xss - ].flatten.freeze - - # All the directives that are not currently in a formal spec, but have - # been implemented somewhere. - DIRECTIVES_DRAFT = [ - :block_all_mixed_content, - ].freeze - - SAFARI_DIRECTIVES = DIRECTIVES_1_0 - - FIREFOX_UNSUPPORTED_DIRECTIVES = [ - :block_all_mixed_content, - :child_src, - :plugin_types - ].freeze - - FIREFOX_DIRECTIVES = ( - DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES - ).freeze - - CHROME_DIRECTIVES = ( - DIRECTIVES_2_0 + DIRECTIVES_DRAFT - ).freeze - - ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort - CONFIG_KEY = :csp - end - - include Constants - - attr_reader :ssl_request - alias :ssl_request? :ssl_request + class ContentSecurityPolicyConfigError < StandardError; end + class ContentSecurityPolicy + MODERN_BROWSERS = %w(Chrome Opera Firefox) + DEFAULT_VALUE = "default-src https:".freeze + DEFAULT_CONFIG = { default_src: %w(https:) }.freeze + HEADER_NAME = "Content-Security-Policy".freeze + REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze + HEADER_NAMES = [HEADER_NAME, REPORT_ONLY] + DATA_PROTOCOL = "data:".freeze + SELF = "'self'".freeze + NONE = "'none'".freeze + STAR = "*".freeze + UNSAFE_INLINE = "'unsafe-inline'".freeze + UNSAFE_EVAL = "'unsafe-eval'".freeze + + SOURCE_VALUES = [ + STAR, + DATA_PROTOCOL, + SELF, + NONE, + UNSAFE_EVAL, + UNSAFE_INLINE + ].freeze + + # leftover deprecated values that will be in common use upon upgrading. + DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze + + DEFAULT_SRC = :default_src + CONNECT_SRC = :connect_src + FONT_SRC = :font_src + FRAME_SRC = :frame_src + IMG_SRC = :img_src + MEDIA_SRC = :media_src + OBJECT_SRC = :object_src + SANDBOX = :sandbox + SCRIPT_SRC = :script_src + STYLE_SRC = :style_src + REPORT_URI = :report_uri + + DIRECTIVES_1_0 = [ + DEFAULT_SRC, + CONNECT_SRC, + FONT_SRC, + FRAME_SRC, + IMG_SRC, + MEDIA_SRC, + OBJECT_SRC, + SANDBOX, + SCRIPT_SRC, + STYLE_SRC, + REPORT_URI + ].freeze + + BASE_URI = :base_uri + CHILD_SRC = :child_src + FORM_ACTION = :form_action + FRAME_ANCESTORS = :frame_ancestors + PLUGIN_TYPES = :plugin_types + + DIRECTIVES_2_0 = [ + DIRECTIVES_1_0, + BASE_URI, + CHILD_SRC, + FORM_ACTION, + FRAME_ANCESTORS, + PLUGIN_TYPES + ].flatten.freeze + + # All the directives currently under consideration for CSP level 3. + # https://w3c.github.io/webappsec/specs/CSP2/ + MANIFEST_SRC = :manifest_src + REFLECTED_XSS = :reflected_xss + DIRECTIVES_3_0 = [ + DIRECTIVES_2_0, + MANIFEST_SRC, + REFLECTED_XSS + ].flatten.freeze + + # All the directives that are not currently in a formal spec, but have + # been implemented somewhere. + BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content + DIRECTIVES_DRAFT = [ + BLOCK_ALL_MIXED_CONTENT + ].freeze + + SAFARI_DIRECTIVES = DIRECTIVES_1_0 + + FIREFOX_UNSUPPORTED_DIRECTIVES = [ + BLOCK_ALL_MIXED_CONTENT, + CHILD_SRC, + PLUGIN_TYPES + ].freeze + + FIREFOX_DIRECTIVES = ( + DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES + ).freeze + + CHROME_DIRECTIVES = ( + DIRECTIVES_2_0 + DIRECTIVES_DRAFT + ).freeze + + ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort + + # Think of default-src and report-uri as the beginning and end respectively, + # everything else is in between. + BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] + + VARIATIONS = { + "Chrome" => CHROME_DIRECTIVES, + "Opera" => CHROME_DIRECTIVES, + "Firefox" => FIREFOX_DIRECTIVES, + "Safari" => SAFARI_DIRECTIVES, + "Other" => CHROME_DIRECTIVES + }.freeze + + OTHER = "Other".freeze + + DIRECTIVE_VALUE_TYPES = { + BASE_URI => :source_list, + BLOCK_ALL_MIXED_CONTENT => :boolean, + CHILD_SRC => :source_list, + CONNECT_SRC => :source_list, + DEFAULT_SRC => :source_list, + FONT_SRC => :source_list, + FORM_ACTION => :source_list, + FRAME_ANCESTORS => :source_list, + FRAME_SRC => :source_list, + IMG_SRC => :source_list, + MANIFEST_SRC => :source_list, + MEDIA_SRC => :source_list, + OBJECT_SRC => :source_list, + PLUGIN_TYPES => :source_list, + REFLECTED_XSS => :string, + REPORT_URI => :source_list, + SANDBOX => :string, + SCRIPT_SRC => :source_list, + STYLE_SRC => :source_list + }.freeze + + CONFIG_KEY = :csp + STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) + HTTP_SCHEME_REGEX = %r{\Ahttps?://} class << self - def generate_nonce - SecureRandom.base64(32).chomp + # Public: generate a header name, value array that is user-agent-aware. + # + # Returns a default policy if no configuration is provided, or a + # header name and value based on the config. + def make_header(config, user_agent) + header = new(config, user_agent) + [header.name, header.value] end - def set_nonce(controller, nonce = generate_nonce) - controller.instance_variable_set(:@content_security_policy_nonce, nonce) + # Public: Validates that the configuration has a valid type, or that it is a valid + # source expression. + # + # Private: validates that a source expression: + # 1. has a valid name + # 2. is an array of strings + # 3. does not contain any depreated, now invalid values (inline, eval, self, none) + # + # Does not validate the invididual values of the source expression (e.g. + # script_src => h*t*t*p: will not raise an exception) + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src] + config.each do |key, value| + if key == :report_only + raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil? + else + validate_directive!(key, value) + end + end end - def add_to_env(request, controller, config) - set_nonce(controller) - options = options_from_request(request).merge(:controller => controller) - request.env[Constants::ENV_KEY] = { - :config => config, - :options => options, - } - end + # Public: combine the values from two different configs. + # + # original - the main config + # additions - values to be merged in + # + # raises an error if the original config is OPT_OUT + # + # 1. for non-source-list values (report_only, block_all_mixed_content), + # additions will overwrite the original value. + # 2. if a value in additions does not exist in the original config, the + # default-src value is included to match original behavior. + # 3. if a value in additions does exist in the original config, the two + # values are joined. + def combine_policies(original, additions) + if original == OPT_OUT + raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") + end - def options_from_request(request) - { - :ssl => request.ssl?, - :ua => request.env['HTTP_USER_AGENT'], - :request_uri => request_uri_from_request(request), - } - end + # in case we would be appending to an empty directive, fill it with the default-src value + additions.keys.each do |directive| + unless original[directive] || !source_list?(directive) + original[directive] = original[:default_src] + end + end - def request_uri_from_request(request) - if request.respond_to?(:original_url) - # rails 3.1+ - request.original_url - else - # rails 2/3.0 - request.url + # merge the two hashes. combine (instead of overwrite) the array values + # when each hash contains a value for a given key. + original.merge(additions) do |directive, lhs, rhs| + if source_list?(directive) + lhs | rhs + else + rhs + end end end - def symbol_to_hyphen_case sym - sym.to_s.gsub('_', '-') - end - end + private - # +options+ param contains - # :controller used for setting instance variables for nonces/hashes - # :ssl_request used to determine if http_additions should be used - # :ua the user agent (or just use Firefox/Chrome/MSIE/etc) - # - # :report used to determine what :ssl_request, :ua, and :request_uri are set to - def initialize(config=nil, options={}) - return unless config - - if options[:request] - options = options.merge(self.class.options_from_request(options[:request])) + def source_list?(directive) + DIRECTIVE_VALUE_TYPES[directive] == :source_list end - @controller = options[:controller] - @ua = options[:ua] - @ssl_request = !!options.delete(:ssl) - @request_uri = options.delete(:request_uri) - @http_additions = config.delete(:http_additions) - @disable_img_src_data_uri = !!config.delete(:disable_img_src_data_uri) - @tag_report_uri = !!config.delete(:tag_report_uri) - @script_hashes = config.delete(:script_hashes) || [] - @app_name = config.delete(:app_name) - @app_name = @app_name.call(@controller) if @app_name.respond_to?(:call) - @enforce = config.delete(:enforce) - @enforce = @enforce.call(@controller) if @enforce.respond_to?(:call) - @enforce = !!@enforce - - # Config values can be string, array, or lamdba values - @config = config.inject({}) do |hash, (key, value)| - config_val = value.respond_to?(:call) ? value.call(@controller) : value - if ALL_DIRECTIVES.include?(key.to_sym) # directives need to be normalized to arrays of strings - config_val = config_val.split if config_val.is_a? String - if config_val.is_a?(Array) - config_val = config_val.map do |val| - translate_dir_value(val) - end.flatten.uniq + # Private: Validates that the configuration has a valid type, or that it is a valid + # source expression. + def validate_directive!(key, value) + case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key] + when :boolean + unless boolean?(value) + raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") + end + when :string + unless value.is_a?(String) + raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value") end - elsif key != :script_hash_middleware - raise ArgumentError.new("Unknown directive supplied: #{key}") + else + validate_source_expression!(key, value) end - - hash[key] = config_val - hash end - # normalize and tag the report-uri - if @config[:report_uri] - @config[:report_uri] = @config[:report_uri].map do |report_uri| - if report_uri.start_with?('//') - report_uri = if @ssl_request - "https:" + report_uri - else - "http:" + report_uri - end - end + # Private: validates that a source expression: + # 1. has a valid name + # 2. is an array of strings + # 3. does not contain any depreated, now invalid values (inline, eval, self, none) + # + # Does not validate the invididual values of the source expression (e.g. + # script_src => h*t*t*p: will not raise an exception) + def validate_source_expression!(key, value) + # source expressions + unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key) + raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}") + end + unless value.is_a?(Array) && value.all? { |v| v.is_a?(String) } + raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings") + end - if @tag_report_uri - report_uri = "#{report_uri}?enforce=#{@enforce}" - report_uri += "&app_name=#{@app_name}" if @app_name + value.each do |source_expression| + if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression) + raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.") end - report_uri end end - add_script_hashes if @script_hashes.any? - strip_unsupported_directives + def boolean?(value) + value.is_a?(TrueClass) || value.is_a?(FalseClass) + end end - ## - # Return or initialize the nonce value used for this header. - # If a reference to a controller is passed in the config, this method - # will check if a nonce has already been set and use it. - def nonce - @nonce ||= @controller.instance_variable_get(:@content_security_policy_nonce) || self.class.generate_nonce + + def initialize(config = nil, user_agent = OTHER) + config = Configuration.deep_copy(DEFAULT_CONFIG) unless config + @config = config + @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base) + user_agent + else + UserAgent.parse(user_agent) + end + @report_only = !!@config[:report_only] + @script_nonce = @config[:script_nonce] + @style_nonce = @config[:style_nonce] end ## # Returns the name to use for the header. Either "Content-Security-Policy" or # "Content-Security-Policy-Report-Only" def name - base = HEADER_NAME - if !@enforce - base += "-Report-Only" + if @report_only + REPORT_ONLY + else + HEADER_NAME end - base end ## # Return the value of the CSP header def value return @config if @config.is_a?(String) - if @config + @value ||= if @config build_value else - DEFAULT_CSP_HEADER + DEFAULT_VALUE end end - def to_json - build_value - @config.inject({}) do |hash, (key, value)| - if ALL_DIRECTIVES.include?(key) - hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value - end - hash - end.to_json - end + private - def self.from_json(*json_configs) - json_configs.inject({}) do |combined_config, one_config| - config = JSON.parse(one_config).inject({}) do |hash, (key, value)| - hash[key.gsub(/(\w+)-(\w+)/, "\\1_\\2").to_sym] = value - hash - end - combined_config.merge(config) do |_, lhs, rhs| - lhs | rhs + # Private: converts the config object into a string representing a policy. + # Places default-src at the first directive and report-uri as the last. All + # others are presented in alphabetical order. + # + # Unsupported directives are filtered based on the user agent. + # + # Returns a content security policy header value. + def build_value + directives.map do |directive_name| + case DIRECTIVE_VALUE_TYPES[directive_name] + when :boolean + symbol_to_hyphen_case(directive_name) + when :string + [symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ") + else + build_directive(directive_name) end - end + end.compact.join("; ") end - private + # Private: builds a string that represents one directive in a minified form. + # If a directive contains *, all other values are omitted. + # If a directive contains 'none' but has other values, 'none' is ommitted. + # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression) + # + # directive_name - a symbol representing the various ALL_DIRECTIVES + # + # Returns a string representing a directive. + def build_directive(directive_name) + source_list = @config[directive_name] + return unless source_list + source_list.compact! + + value = if source_list.include?(STAR) + # Discard trailing entries since * accomplishes the same. + STAR + else + populate_nonces(directive_name, source_list) - def add_script_hashes - @config[:script_src] << @script_hashes.map {|hash| "'#{hash}'"} << ["'unsafe-inline'"] - end + # Discard any 'none' values if more directives are supplied since none may override values. + source_list.reject! { |value| value == NONE } if source_list.length > 1 - def build_value - raise "Expected to find default_src directive value" unless @config[:default_src] - append_http_additions unless ssl_request? - generic_directives - end - - def append_http_additions - return unless @http_additions - @http_additions.each do |k, v| - @config[k] ||= [] - @config[k] << v + # remove schemes and dedup source expressions + source_list = strip_source_schemes(source_list) unless directive_name == REPORT_URI + dedup_source_list(source_list).join(" ") end + [symbol_to_hyphen_case(directive_name), value].join(" ") end - def translate_dir_value val - if %w{inline eval}.include?(val) - warn "[DEPRECATION] using inline/eval may not be supported in the future. Instead use 'unsafe-inline'/'unsafe-eval' instead." - val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'" - elsif %{self none}.include?(val) - warn "[DEPRECATION] using self/none may not be supported in the future. Instead use 'self'/'none' instead." - "'#{val}'" - elsif val == 'nonce' - if supports_nonces? - self.class.set_nonce(@controller, nonce) - ["'nonce-#{nonce}'", "'unsafe-inline'"] - else - "'unsafe-inline'" + # Removes duplicates and sources that already match an existing wild card. + # + # e.g. *.github.com asdf.github.com becomes *.github.com + def dedup_source_list(sources) + sources = sources.uniq + wild_sources = sources.select { |source| source =~ STAR_REGEXP } + + if wild_sources.any? + sources.reject do |source| + !wild_sources.include?(source) && + wild_sources.any? { |pattern| File.fnmatch(pattern, source) } end else - val + sources end end - # ensures defualt_src is first and report_uri is last - def generic_directives - header_value = build_directive(:default_src) - data_uri = @disable_img_src_data_uri ? [] : ["data:"] - if @config[:img_src] - @config[:img_src] = @config[:img_src] + data_uri unless @config[:img_src].include?('data:') - else - @config[:img_src] = @config[:default_src] + data_uri + # Private: append a nonce to the script/style directories if script_nonce + # or style_nonce are provided. + def populate_nonces(directive, source_list) + case directive + when SCRIPT_SRC + append_nonce(source_list, @script_nonce) + when STYLE_SRC + append_nonce(source_list, @style_nonce) end + end - (ALL_DIRECTIVES - [:default_src, :report_uri]).each do |directive_name| - if @config[directive_name] - header_value += build_directive(directive_name) + # Private: adds a nonce or 'unsafe-inline' depending on browser support. + # If a nonce is populated, inline content is assumed. + # + # While CSP is backward compatible in that a policy with a nonce will ignore + # unsafe-inline, this is more concise. + def append_nonce(source_list, nonce) + if nonce + if nonces_supported? + source_list << "'nonce-#{nonce}'" + else + source_list << UNSAFE_INLINE end end - - header_value += build_directive(:report_uri) if @config[:report_uri] - - header_value.strip end - def build_directive(key) - "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; " + # Private: return the list of directives that are supported by the user agent, + # starting with default-src and ending with report-uri. + def directives + [DEFAULT_SRC, + BODY_DIRECTIVES.select { |key| supported_directives.include?(key) }.select { |directive| @config.key?(directive) }, + REPORT_URI].flatten end - def strip_unsupported_directives - @config.select! { |key, _| supported_directives.include?(key) } + # Private: Remove scheme from source expressions. + def strip_source_schemes(source_list) + source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } end + # Private: determine which directives are supported for the given user agent. + # + # Returns an array of symbols representing the directives. def supported_directives - @supported_directives ||= case UserAgentParser.parse(@ua).family - when "Chrome" - CHROME_DIRECTIVES - when "Safari" - SAFARI_DIRECTIVES - when "Firefox" - FIREFOX_DIRECTIVES - else - DIRECTIVES_1_0 - end + @supported_directives ||= VARIATIONS[@parsed_ua.browser] || VARIATIONS[OTHER] + end + + def nonces_supported? + @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser) end - def supports_nonces? - parsed_ua = UserAgentParser.parse(@ua) - ["Chrome", "Opera", "Firefox"].include?(parsed_ua.family) + def symbol_to_hyphen_case(sym) + sym.to_s.tr('_', '-') end end end diff --git a/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb b/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb deleted file mode 100644 index 33819664..00000000 --- a/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +++ /dev/null @@ -1,22 +0,0 @@ -module SecureHeaders - class ContentSecurityPolicy - class ScriptHashMiddleware - def initialize(app) - @app = app - end - - def call(env) - status, headers, response = @app.call(env) - metadata = env[ContentSecurityPolicy::ENV_KEY] - if !metadata.nil? - config, options = metadata.values_at(:config, :options) - config.merge!(:script_hashes => env[HASHES_ENV_KEY]) - csp_header = ContentSecurityPolicy.new(config, options) - headers[csp_header.name] = csp_header.value - end - - [status, headers, response] - end - end - end -end diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb index 1aa453b2..1016a6e2 100644 --- a/lib/secure_headers/headers/public_key_pins.rb +++ b/lib/secure_headers/headers/public_key_pins.rb @@ -1,63 +1,62 @@ module SecureHeaders - class PublicKeyPinsBuildError < StandardError; end - class PublicKeyPins < Header - module Constants - HPKP_HEADER_NAME = "Public-Key-Pins" - ENV_KEY = 'secure_headers.public_key_pins' - HASH_ALGORITHMS = [:sha256] - DIRECTIVES = [:max_age] - CONFIG_KEY = :hpkp - end + class PublicKeyPinsConfigError < StandardError; end + class PublicKeyPins + HEADER_NAME = "Public-Key-Pins".freeze + REPORT_ONLY = "Public-Key-Pins-Report-Only".freeze + HASH_ALGORITHMS = [:sha256].freeze + CONFIG_KEY = :hpkp + + class << self - def symbol_to_hyphen_case sym - sym.to_s.gsub('_', '-') + # Public: make an hpkp header name, value pair + # + # Returns nil if not configured, returns header name and value if configured. + def make_header(config) + return if config.nil? + header = new(config) + [header.name, header.value] end - end - include Constants - def initialize(config=nil) - @config = validate_config(config) + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise PublicKeyPinsConfigError.new("config must be a hash.") unless config.is_a? Hash + + if !config[:max_age] + raise PublicKeyPinsConfigError.new("max-age is a required directive.") + elsif config[:max_age].to_s !~ /\A\d+\z/ + raise PublicKeyPinsConfigError.new("max-age must be a number. + #{config[:max_age]} was supplied.") + elsif config[:pins] && config[:pins].length < 2 + raise PublicKeyPinsConfigError.new("A minimum of 2 pins are required.") + end + end + end - @pins = @config.fetch(:pins, nil) - @report_uri = @config.fetch(:report_uri, nil) - @app_name = @config.fetch(:app_name, nil) - @enforce = !!@config.fetch(:enforce, nil) - @include_subdomains = !!@config.fetch(:include_subdomains, nil) - @tag_report_uri = !!@config.fetch(:tag_report_uri, nil) + def initialize(config) + @max_age = config.fetch(:max_age, nil) + @pins = config.fetch(:pins, nil) + @report_uri = config.fetch(:report_uri, nil) + @report_only = !!config.fetch(:report_only, nil) + @include_subdomains = !!config.fetch(:include_subdomains, nil) end def name - base = HPKP_HEADER_NAME - if !@enforce - base += "-Report-Only" + if @report_only + REPORT_ONLY + else + HEADER_NAME end - base end def value header_value = [ - generic_directives, + max_age_directive, pin_directives, report_uri_directive, subdomain_directive ].compact.join('; ').strip end - def validate_config(config) - raise PublicKeyPinsBuildError.new("config must be a hash.") unless config.is_a? Hash - - if !config[:max_age] - raise PublicKeyPinsBuildError.new("max-age is a required directive.") - elsif config[:max_age].to_s !~ /\A\d+\z/ - raise PublicKeyPinsBuildError.new("max-age must be a number. - #{config[:max_age]} was supplied.") - elsif config[:pins] && config[:pins].length < 2 - raise PublicKeyPinsBuildError.new("A minimum of 2 pins are required.") - end - - config - end - def pin_directives return nil if @pins.nil? @pins.collect do |pin| @@ -67,28 +66,14 @@ def pin_directives end.join('; ') end - def generic_directives - DIRECTIVES.collect do |directive_name| - build_directive(directive_name) if @config[directive_name] - end.join('; ') - end - - def build_directive(key) - "#{self.class.symbol_to_hyphen_case(key)}=#{@config[key]}" + def max_age_directive + "max-age=#{@max_age}" if @max_age end def report_uri_directive - return nil if @report_uri.nil? - - if @tag_report_uri - @report_uri = "#{@report_uri}?enforce=#{@enforce}" - @report_uri += "&app_name=#{@app_name}" if @app_name - end - - "report-uri=\"#{@report_uri}\"" + "report-uri=\"#{@report_uri}\"" if @report_uri end - def subdomain_directive @include_subdomains ? 'includeSubDomains' : nil end diff --git a/lib/secure_headers/headers/strict_transport_security.rb b/lib/secure_headers/headers/strict_transport_security.rb index b5114fcb..9ec72748 100644 --- a/lib/secure_headers/headers/strict_transport_security.rb +++ b/lib/secure_headers/headers/strict_transport_security.rb @@ -1,54 +1,27 @@ module SecureHeaders - class STSBuildError < StandardError; end - - class StrictTransportSecurity < Header - module Constants - HSTS_HEADER_NAME = 'Strict-Transport-Security' - HSTS_MAX_AGE = "631138519" - DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE - VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i - MESSAGE = "The config value supplied for the HSTS header was invalid." - CONFIG_KEY = :hsts - end - include Constants - - def initialize(config = nil) - @config = config - validate_config unless @config.nil? - end - - def name - return HSTS_HEADER_NAME - end - - def value - case @config - when String - return @config - when NilClass - return DEFAULT_VALUE + class STSConfigError < StandardError; end + + class StrictTransportSecurity + HEADER_NAME = 'Strict-Transport-Security' + HSTS_MAX_AGE = "631138519" + DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE + VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i + MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}" + CONFIG_KEY = :hsts + + class << self + # Public: generate an hsts header name, value pair. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def make_header(config = nil) + [HEADER_NAME, config || DEFAULT_VALUE] end - max_age = @config.fetch(:max_age, HSTS_MAX_AGE) - value = "max-age=" + max_age.to_s - value += "; includeSubdomains" if @config[:include_subdomains] - value += "; preload" if @config[:preload] - - value - end - - private - - def validate_config - if @config.is_a? Hash - if !@config[:max_age] - raise STSBuildError.new("No max-age was supplied.") - elsif @config[:max_age].to_s !~ /\A\d+\z/ - raise STSBuildError.new("max-age must be a number. #{@config[:max_age]} was supplied.") - end - else - @config = @config.to_s - raise STSBuildError.new(MESSAGE) unless @config =~ VALID_STS_HEADER + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config} #{config.class}") unless config.is_a?(String) + raise STSConfigError.new(MESSAGE) unless config =~ VALID_STS_HEADER end end end diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index e832d0d3..76796db2 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -1,40 +1,26 @@ module SecureHeaders - class XContentTypeOptionsBuildError < StandardError; end + class XContentTypeOptionsConfigError < StandardError; end # IE only - class XContentTypeOptions < Header - module Constants - X_CONTENT_TYPE_OPTIONS_HEADER_NAME = "X-Content-Type-Options" - DEFAULT_VALUE = "nosniff" - CONFIG_KEY = :x_content_type_options - end - include Constants - - def initialize(config=nil) - @config = config - validate_config unless @config.nil? - end + class XContentTypeOptions + HEADER_NAME = "X-Content-Type-Options" + DEFAULT_VALUE = "nosniff" + CONFIG_KEY = :x_content_type_options - def name - X_CONTENT_TYPE_OPTIONS_HEADER_NAME - end - - def value - case @config - when NilClass - DEFAULT_VALUE - when String - @config - else - @config[:value] + class << self + # Public: generate an X-Content-Type-Options header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def make_header(config = nil) + [HEADER_NAME, config || DEFAULT_VALUE] end - end - - private - def validate_config - value = @config.is_a?(Hash) ? @config[:value] : @config - unless value.casecmp(DEFAULT_VALUE) == 0 - raise XContentTypeOptionsBuildError.new("Value can only be nil or 'nosniff'") + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless config.casecmp(DEFAULT_VALUE) == 0 + raise XContentTypeOptionsConfigError.new("Value can only be nil or 'nosniff'") + end end end end diff --git a/lib/secure_headers/headers/x_download_options.rb b/lib/secure_headers/headers/x_download_options.rb index 77d2401d..cabf1feb 100644 --- a/lib/secure_headers/headers/x_download_options.rb +++ b/lib/secure_headers/headers/x_download_options.rb @@ -1,39 +1,25 @@ module SecureHeaders - class XDOBuildError < StandardError; end - class XDownloadOptions < Header - module Constants - XDO_HEADER_NAME = "X-Download-Options" - DEFAULT_VALUE = 'noopen' - CONFIG_KEY = :x_download_options - end - include Constants - - def initialize(config = nil) - @config = config - validate_config unless @config.nil? - end + class XDOConfigError < StandardError; end + class XDownloadOptions + HEADER_NAME = "X-Download-Options" + DEFAULT_VALUE = 'noopen' + CONFIG_KEY = :x_download_options - def name - XDO_HEADER_NAME - end - - def value - case @config - when NilClass - DEFAULT_VALUE - when String - @config - else - @config[:value] + class << self + # Public: generate an X-Download-Options header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def make_header(config = nil) + [HEADER_NAME, config || DEFAULT_VALUE] end - end - - private - def validate_config - value = @config.is_a?(Hash) ? @config[:value] : @config - unless value.casecmp(DEFAULT_VALUE) == 0 - raise XDOBuildError.new("Value can only be nil or 'noopen'") + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless config.casecmp(DEFAULT_VALUE) == 0 + raise XDOConfigError.new("Value can only be nil or 'noopen'") + end end end end diff --git a/lib/secure_headers/headers/x_frame_options.rb b/lib/secure_headers/headers/x_frame_options.rb index 2014dba4..ab6fb3fc 100644 --- a/lib/secure_headers/headers/x_frame_options.rb +++ b/lib/secure_headers/headers/x_frame_options.rb @@ -1,40 +1,31 @@ module SecureHeaders - class XFOBuildError < StandardError; end - class XFrameOptions < Header - module Constants - XFO_HEADER_NAME = "X-Frame-Options" - DEFAULT_VALUE = 'SAMEORIGIN' - VALID_XFO_HEADER = /\A(SAMEORIGIN\z|DENY\z|ALLOW-FROM[:\s])/i - CONFIG_KEY = :x_frame_options - end - include Constants - - def initialize(config = nil) - @config = config - validate_config unless @config.nil? - end + class XFOConfigError < StandardError; end + class XFrameOptions + HEADER_NAME = "X-Frame-Options" + CONFIG_KEY = :x_frame_options + SAMEORIGIN = "sameorigin" + DENY = "deny" + ALLOW_FROM = "allow-from" + ALLOW_ALL = "allowall" + DEFAULT_VALUE = SAMEORIGIN + VALID_XFO_HEADER = /\A(#{SAMEORIGIN}\z|#{DENY}\z|#{ALLOW_ALL}\z|#{ALLOW_FROM}[:\s])/i - def name - XFO_HEADER_NAME - end - - def value - case @config - when NilClass - DEFAULT_VALUE - when String - @config - else - @config[:value] + class << self + # Public: generate an X-Frame-Options header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def make_header(config = nil) + return if config == OPT_OUT + [HEADER_NAME, config || DEFAULT_VALUE] end - end - - private - def validate_config - value = @config.is_a?(Hash) ? @config[:value] : @config - unless value =~ VALID_XFO_HEADER - raise XFOBuildError.new("Value must be SAMEORIGIN|DENY|ALLOW-FROM:") + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless config =~ VALID_XFO_HEADER + raise XFOConfigError.new("Value must be SAMEORIGIN|DENY|ALLOW-FROM:|ALLOWALL") + end end end end diff --git a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb index b92a62ef..acac98aa 100644 --- a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +++ b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb @@ -1,40 +1,26 @@ module SecureHeaders - class XPCDPBuildError < StandardError; end - class XPermittedCrossDomainPolicies < Header - module Constants - XPCDP_HEADER_NAME = "X-Permitted-Cross-Domain-Policies" - DEFAULT_VALUE = 'none' - VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) - CONFIG_KEY = :x_permitted_cross_domain_policies - end - include Constants - - def initialize(config = nil) - @config = config - validate_config unless @config.nil? - end + class XPCDPConfigError < StandardError; end + class XPermittedCrossDomainPolicies + HEADER_NAME = "X-Permitted-Cross-Domain-Policies" + DEFAULT_VALUE = 'none' + VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) + CONFIG_KEY = :x_permitted_cross_domain_policies - def name - XPCDP_HEADER_NAME - end - - def value - case @config - when NilClass - DEFAULT_VALUE - when String - @config - else - @config[:value] + class << self + # Public: generate an X-Permitted-Cross-Domain-Policies header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def make_header(config = nil) + [HEADER_NAME, config || DEFAULT_VALUE] end - end - - private - def validate_config - value = @config.is_a?(Hash) ? @config[:value] : @config - unless VALID_POLICIES.include?(value.downcase) - raise XPCDPBuildError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless VALID_POLICIES.include?(config.downcase) + raise XPCDPConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") + end end end end diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index fee7c167..ec8b261f 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -1,54 +1,24 @@ module SecureHeaders - class XXssProtectionBuildError < StandardError; end - class XXssProtection < Header - module Constants - X_XSS_PROTECTION_HEADER_NAME = 'X-XSS-Protection' - DEFAULT_VALUE = "1" - VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i - CONFIG_KEY = :x_xss_protection - end - include Constants - - def initialize(config=nil) - @config = config - validate_config unless @config.nil? - end - - def name - X_XSS_PROTECTION_HEADER_NAME - end + class XXssProtectionConfigError < StandardError; end + class XXssProtection + HEADER_NAME = 'X-XSS-Protection' + DEFAULT_VALUE = "1; mode=block" + VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i + CONFIG_KEY = :x_xss_protection - def value - case @config - when NilClass - DEFAULT_VALUE - when String - @config - else - value = @config[:value].to_s - value += "; mode=#{@config[:mode]}" if @config[:mode] - value += "; report=#{@config[:report_uri]}" if @config[:report_uri] - value + class << self + # Public: generate an X-Xss-Protection header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def make_header(config = nil) + [HEADER_NAME, config || DEFAULT_VALUE] end - end - - private - - def validate_config - if @config.is_a? Hash - if !@config[:value] - raise XXssProtectionBuildError.new(":value key is missing") - elsif @config[:value] - unless [0,1].include?(@config[:value].to_i) - raise XXssProtectionBuildError.new(":value must be 1 or 0") - end - if @config[:mode] && @config[:mode].casecmp('block') != 0 - raise XXssProtectionBuildError.new(":mode must nil or 'block'") - end - end - elsif @config.is_a? String - raise XXssProtectionBuildError.new("Invalid format (see VALID_X_XSS_HEADER)") unless @config =~ VALID_X_XSS_HEADER + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + raise XXssProtectionConfigError.new("Invalid format (see VALID_X_XSS_HEADER)") unless config.to_s =~ VALID_X_XSS_HEADER end end end diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb new file mode 100644 index 00000000..02e8472e --- /dev/null +++ b/lib/secure_headers/middleware.rb @@ -0,0 +1,15 @@ +module SecureHeaders + class Middleware + def initialize(app) + @app = app + end + + # merges the hash of headers into the current header set. + def call(env) + req = Rack::Request.new(env) + status, headers, response = @app.call(env) + headers.merge!(SecureHeaders.header_hash_for(req)) + [status, headers, response] + end + end +end diff --git a/lib/secure_headers/padrino.rb b/lib/secure_headers/padrino.rb index 6e2cd954..5a8904db 100644 --- a/lib/secure_headers/padrino.rb +++ b/lib/secure_headers/padrino.rb @@ -5,10 +5,9 @@ class << self # Main class that register this extension. # def registered(app) - app.extend SecureHeaders::ClassMethods app.helpers SecureHeaders::InstanceMethods end - alias :included :registered + alias_method :included, :registered end end end diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index 5530e434..04bab46d 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -1,24 +1,27 @@ # rails 3.1+ if defined?(Rails::Railtie) module SecureHeaders - class Railtie < Rails::Engine - isolate_namespace ::SecureHeaders if defined? isolate_namespace # rails 3.0 + class Railtie < Rails::Railtie + isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0 conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', 'X-Content-Type-Options', 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options', 'X-Content-Type-Options', 'Strict-Transport-Security', 'Content-Security-Policy', 'Content-Security-Policy-Report-Only', - 'X-Permitted-Cross-Domain-Policies','Public-Key-Pins','Public-Key-Pins-Report-Only'] + 'X-Permitted-Cross-Domain-Policies', 'Public-Key-Pins', 'Public-Key-Pins-Report-Only'] + + initializer "secure_headers.middleware" do + Rails.application.config.middleware.use SecureHeaders::Middleware + end initializer "secure_headers.action_controller" do ActiveSupport.on_load(:action_controller) do - include ::SecureHeaders + include SecureHeaders unless Rails.application.config.action_dispatch.default_headers.nil? conflicting_headers.each do |header| Rails.application.config.action_dispatch.default_headers.delete(header) end end - end end end @@ -26,7 +29,7 @@ class Railtie < Rails::Engine else module ActionController class Base - include ::SecureHeaders + include SecureHeaders end end end diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb deleted file mode 100644 index 5251d57f..00000000 --- a/lib/secure_headers/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module SecureHeaders - VERSION = "2.4.1" -end diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index a519333d..dd410b28 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -1,61 +1,45 @@ module SecureHeaders - class UnexpectedHashedScriptException < StandardError - - end - module ViewHelpers - include SecureHeaders::HashHelper - SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes" - - def nonced_style_tag(content = nil, &block) - nonced_tag(content, :style, block) + # Public: create a style tag using the content security policy nonce. + # Instructs secure_headers to append a nonce to style/script-src directives. + # + # Returns an html-safe style tag with the nonce attribute. + def nonced_style_tag(content_or_options, &block) + nonced_tag(:style, content_or_options, block) end - def nonced_javascript_tag(content = nil, &block) - nonced_tag(content, :script, block) + # Public: create a script tag using the content security policy nonce. + # Instructs secure_headers to append a nonce to style/script-src directives. + # + # Returns an html-safe script tag with the nonce attribute. + def nonced_javascript_tag(content_or_options, &block) + nonced_tag(:script, content_or_options, block) end - def hashed_javascript_tag(raise_error_on_unrecognized_hash = false, &block) - content = capture(&block) - - if ['development', 'test'].include?(ENV["RAILS_ENV"]) - hash_value = hash_source(content) - file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb') - script_hashes = controller.instance_variable_get(:@script_hashes)[file_path] - unless script_hashes && script_hashes.include?(hash_value) - message = unexpected_hash_error_message(file_path, hash_value, content) - if raise_error_on_unrecognized_hash - raise UnexpectedHashedScriptException.new(message) - else - request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value - end - end + # Public: use the content security policy nonce for this request directly. + # Instructs secure_headers to append a nonce to style/script-src directives. + # + # Returns a non-html-safe nonce value. + def content_security_policy_nonce(type) + case type + when :script + SecureHeaders.content_security_policy_script_nonce(@_request) + when :style + SecureHeaders.content_security_policy_style_nonce(@_request) end - - content_tag :script, content end private - def nonced_tag(content, type, block) + def nonced_tag(type, content_or_options, block) + options = {} content = if block + options = content_or_options capture(&block) else - content.html_safe # :'( + content_or_options.html_safe # :'( end - - content_tag type, content, :nonce => @content_security_policy_nonce - end - - def unexpected_hash_error_message(file_path, hash_value, content) - <<-EOF -\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} *** - -*** This is fine in dev/test, but will raise exceptions in production. *** -*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:*** -#{file_path}: -- #{hash_value}\n\n - EOF + content_tag type, content, options.merge(nonce: content_security_policy_nonce(type)) end end end diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake deleted file mode 100644 index 8ca10bab..00000000 --- a/lib/tasks/tasks.rake +++ /dev/null @@ -1,48 +0,0 @@ -INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx -INLINE_HASH_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx -SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml' - -namespace :secure_headers do - include SecureHeaders::HashHelper - - def is_erb?(filename) - filename =~ /\.erb\Z/ - end - - def generate_inline_script_hashes(filename) - file = File.read(filename) - hashes = [] - - [INLINE_SCRIPT_REGEX, INLINE_HASH_HELPER_REGEX].each do |regex| - file.gsub(regex) do # TODO don't use gsub - inline_script = Regexp.last_match.captures.last - if (filename =~ /\.mustache\Z/ && inline_script =~ /\{\{.*\}\}/) || (is_erb?(filename) && inline_script =~ /<%.*%>/) - puts "Looks like there's some dynamic content inside of a script tag :-/" - puts "That pretty much means the hash value will never match." - puts "Code: " + inline_script - puts "=" * 20 - end - - hashes << hash_source(inline_script) - end - end - - hashes - end - - task :generate_hashes do |t, args| - script_hashes = {} - Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename| - hashes = generate_inline_script_hashes(filename) - if hashes.any? - script_hashes[filename] = hashes - end - end - - File.open(SCRIPT_HASH_CONFIG_FILE, 'w') do |file| - file.write(script_hashes.to_yaml) - end - - puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SCRIPT_HASH_CONFIG_FILE}" - end -end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 32c46b31..1d9b1078 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,23 +1,19 @@ # -*- encoding: utf-8 -*- -lib = File.expand_path('../lib', __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'secure_headers/version' - Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = SecureHeaders::VERSION + gem.version = "3.0.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] - gem.description = %q{Security related headers all in one gem.} - gem.summary = %q{Add easily configured security headers to responses + gem.description = 'Security related headers all in one gem.' + gem.summary = 'Add easily configured security headers to responses including content-security-policy, x-frame-options, - strict-transport-security, etc.} + strict-transport-security, etc.' gem.homepage = "https://github.com/twitter/secureheaders" gem.license = "Apache Public License 2.0" - gem.files = `git ls-files`.split($/) - gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) + gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_development_dependency "rake" - gem.add_dependency "user_agent_parser" + gem.add_dependency "useragent" end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb new file mode 100644 index 00000000..59d9d5a2 --- /dev/null +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +module SecureHeaders + describe Configuration do + before(:each) do + reset_config + Configuration.default + end + + it "has a default config" do + expect(Configuration.get(Configuration::DEFAULT_CONFIG)).to_not be_nil + end + + it "has an 'noop' config" do + expect(Configuration.get(Configuration::NOOP_CONFIGURATION)).to_not be_nil + end + + it "precomputes headers upon creation" do + default_config = Configuration.get(Configuration::DEFAULT_CONFIG) + header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash| + header_name, header_value = if key == :csp + value["Chrome"] + else + value + end + + hash[header_name] = header_value + end + expect_default_values(header_hash) + end + + it "copies all config values except for the cached headers when dup" do + Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do + # do nothing, just copy it + end + + config = Configuration.get(:test_override) + noop = Configuration.get(Configuration::NOOP_CONFIGURATION) + [:hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, + :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp].each do |key| + + expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}." + end + end + + it "stores an override of the global config" do + Configuration.override(:test_override) do |config| + config.x_frame_options = "DENY" + end + + expect(Configuration.get(:test_override)).to_not be_nil + end + + it "deep dup's config values when overriding so the original cannot be modified" do + Configuration.override(:override) do |config| + config.csp[:default_src] << "'self'" + end + + default = Configuration.get + override = Configuration.get(:override) + + expect(override.csp).not_to eq(default.csp) + end + + it "allows you to override an override" do + Configuration.override(:override) do |config| + config.csp = { default_src: %w('self')} + end + + Configuration.override(:second_override, :override) do |config| + config.csp = config.csp.merge(script_src: %w(example.org)) + end + + original_override = Configuration.get(:override) + expect(original_override.csp).to eq(default_src: %w('self')) + override_config = Configuration.get(:second_override) + expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org)) + end + end +end diff --git a/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb b/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb deleted file mode 100644 index 2a4b7654..00000000 --- a/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'spec_helper' -require 'secure_headers/headers/content_security_policy/script_hash_middleware' - -module SecureHeaders - describe ContentSecurityPolicy::ScriptHashMiddleware do - - let(:app) { double(:call => [200, headers, '']) } - let(:env) { double } - let(:headers) { double } - - let(:default_config) do - { - :default_src => 'https://*', - :report_uri => '/csp_report', - :script_src => "'unsafe-inline' 'unsafe-eval' https://* data:", - :style_src => "'unsafe-inline' https://* about:" - } - end - - def should_assign_header name, value - expect(headers).to receive(:[]=).with(name, value) - end - - def call_middleware(hashes = []) - options = { - :ua => USER_AGENTS[:chrome] - } - expect(env).to receive(:[]).with(HASHES_ENV_KEY).and_return(hashes) - expect(env).to receive(:[]).with(ENV_KEY).and_return( - :config => default_config, - :options => options - ) - ContentSecurityPolicy::ScriptHashMiddleware.new(app).call(env) - end - - it "adds hashes stored in env to the header" do - should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*'sha256-/) - call_middleware(['sha256-abc123']) - end - - it "leaves things alone when no hashes are saved to env" do - should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*(?!'sha256-)/) - call_middleware() - end - end -end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index d8fb6150..b2b784f4 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -2,328 +2,171 @@ module SecureHeaders describe ContentSecurityPolicy do - let(:default_opts) do + let (:default_opts) do { - :default_src => 'https:', - :img_src => "https: data:", - :script_src => "'unsafe-inline' 'unsafe-eval' https: data:", - :style_src => "'unsafe-inline' https: about:", - :report_uri => '/csp_report' + default_src: %w(https:), + img_src: %w(https: data:), + script_src: %w('unsafe-inline' 'unsafe-eval' https: data:), + style_src: %w('unsafe-inline' https: about:), + report_uri: %w(/csp_report) } end - let(:controller) { DummyClass.new } - - IE = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)" - FIREFOX = "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8" - FIREFOX_23 = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0" - CHROME = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4" - CHROME_25 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22" - SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A" - OPERA = "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16" - - def request_for user_agent, request_uri=nil, options={:ssl => false} - double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') ) - end - - before(:each) do - @options_with_forwarding = default_opts.merge(:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com') - end describe "#name" do - context "when supplying options to override request" do - specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")} - specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")} - specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")} - specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")} - specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")} - end - context "when in report-only mode" do - specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(HEADER_NAME + "-Report-Only")} - specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME + "-Report-Only")} - specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME + "-Report-Only")} - specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")} - specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")} + specify { expect(ContentSecurityPolicy.new(default_opts.merge(:report_only => true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") } end context "when in enforce mode" do - let(:opts) { default_opts.merge(:enforce => true)} - - specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(HEADER_NAME)} - specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME)} - specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME)} - specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME)} - specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)} + specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicy::HEADER_NAME) } end end - it "exports a policy to JSON" do - policy = ContentSecurityPolicy.new(default_opts) - expected = %({"default-src":["https:"],"img-src":["https:","data:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"report-uri":["/csp_report"]}) - expect(policy.to_json).to eq(expected) - end - - it "imports JSON to build a policy" do - json1 = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"]}) - json2 = %({"style-src":["'unsafe-inline'"],"img-src":["https:","data:"]}) - json3 = %({"style-src":["https:","about:"]}) - config = ContentSecurityPolicy.from_json(json1, json2, json3) - policy = ContentSecurityPolicy.new(config) + describe "#validate_config!" do + it "requires a :default_src value" do + expect do + CSP.validate_config!(script_src: %('self')) + end.to raise_error(ContentSecurityPolicyConfigError) + end - expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]}) - expect(policy.to_json).to eq(expected) - end + it "requires :report_only to be a truthy value" do + expect do + CSP.validate_config!(default_opts.merge(report_only: "steve")) + end.to raise_error(ContentSecurityPolicyConfigError) + end - context "when using hash sources" do - it "adds hashes and unsafe-inline to the script-src" do - policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123'])) - expect(policy.value).to match /script-src[^;]*'sha256-abc123'/ + it "requires :block_all_mixed_content to be a boolean value" do + expect do + CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve")) + end.to raise_error(ContentSecurityPolicyConfigError) end - end - describe "#normalize_csp_options" do - before(:each) do - default_opts[:script_src] << " 'self' 'none'" - @opts = default_opts + it "requires all source lists to be an array of strings" do + expect do + CSP.validate_config!(default_src: "steve") + end.to raise_error(ContentSecurityPolicyConfigError) end - context "Content-Security-Policy" do - it "converts the script values to their equivilents" do - csp = ContentSecurityPolicy.new(@opts, :request => request_for(CHROME)) - expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https: data: 'self' 'none'") - end + it "rejects unknown directives / config" do + expect do + CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve") + end.to raise_error(ContentSecurityPolicyConfigError) + end - it "adds a @enforce and @app_name variables to the report uri" do - opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => proc { 'twitter' }) - csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME)) - expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter") - end + # this is mostly to ensure people don't use the antiquated shorthands common in other configs + it "performs light validation on source lists" do + expect do + CSP.validate_config!(default_src: %w(self none inline eval)) + end.to raise_error(ContentSecurityPolicyConfigError) + end + end - it "does not add an empty @app_name variable to the report uri" do - opts = @opts.merge(:tag_report_uri => true, :enforce => true) - csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME)) - expect(csp.value).to include("/csp_report?enforce=true") - end + describe "#combine_policies" do + it "combines the default-src value with the override if the directive was unconfigured" do + combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com)) + csp = ContentSecurityPolicy.new(combined_config) + expect(csp.name).to eq(CSP::HEADER_NAME) + expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") + end - it "accepts procs for report-uris" do - opts = { - :default_src => "'self'", - :report_uri => proc { "http://lambda/result" } + it "overrides the report_only flag" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + report_only: false } - - csp = ContentSecurityPolicy.new(opts) - expect(csp.value).to match("report-uri http://lambda/result") end + combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true) + csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) + expect(csp.name).to eq(CSP::REPORT_ONLY) + end - it "accepts procs for other fields" do - opts = { - :default_src => proc { "http://lambda/result" }, - :enforce => proc { true }, + it "overrides the :block_all_mixed_content flag" do + Configuration.default do |config| + config.csp = { + default_src: %w(https:), + block_all_mixed_content: false } - - csp = ContentSecurityPolicy.new(opts) - expect(csp.value).to eq("default-src http://lambda/result; img-src http://lambda/result data:;") - expect(csp.name).to match("Content-Security-Policy") end + combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true) + csp = ContentSecurityPolicy.new(combined_config) + expect(csp.value).to eq("default-src https:; block-all-mixed-content") + end - it "passes a reference to the controller to the proc" do - controller = double - user = double(:beta_testing? => true) - - allow(controller).to receive(:current_user).and_return(user) - opts = { - :default_src => "'self'", - :enforce => lambda { |c| c.current_user.beta_testing? } - } - csp = ContentSecurityPolicy.new(opts, :controller => controller) - expect(csp.name).to match("Content-Security-Policy") + it "raises an error if appending to a OPT_OUT policy" do + Configuration.default do |config| + config.csp = OPT_OUT end + expect do + CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com)) + end.to raise_error(ContentSecurityPolicyConfigError) end end describe "#value" do - context "browser sniffing" do - let(:complex_opts) do - ALL_DIRECTIVES.inject({}) { |memo, directive| memo[directive] = "'self'"; memo }.merge(:block_all_mixed_content => '') - end - - it "does not filter any directives for Chrome" do - policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(CHROME)) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content ; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") - end - - it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do - policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(FIREFOX)) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") - end - - it "filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, and plugin-types for safari" do - policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(SAFARI)) - expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';") - end + it "discards 'none' values if any other source expressions are present" do + csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none'))) + expect(csp.value).not_to include("'none'") end - it "raises an exception when default-src is missing" do - csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME)) - expect { - csp.value - }.to raise_error(RuntimeError) + it "discards any other source expressions when * is present" do + csp = ContentSecurityPolicy.new(default_src: %w(* http: https: example.org)) + expect(csp.value).to eq("default-src *") end - context "auto-whitelists data: uris for img-src" do - it "sets the value if no img-src specified" do - csp = ContentSecurityPolicy.new({:default_src => "'self'"}, :request => request_for(CHROME)) - expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") - end - - it "appends the value if img-src is specified" do - csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self'"}, :request => request_for(CHROME)) - expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") - end - - it "doesn't add a duplicate data uri if img-src specifies it already" do - csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self' data:"}, :request => request_for(CHROME)) - expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;") - end - - it "allows the user to disable img-src data: uris auto-whitelisting" do - csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self'", :disable_img_src_data_uri => true}, :request => request_for(CHROME)) - expect(csp.value).to eq("default-src 'self'; img-src 'self';") - end + it "minifies source expressions based on overlapping wildcards" do + config = { + default_src: %w(a.example.org b.example.org *.example.org https://*.example.org) + } + csp = ContentSecurityPolicy.new(config) + expect(csp.value).to eq("default-src *.example.org") end - it "sends the standard csp header if an unknown browser is supplied" do - csp = ContentSecurityPolicy.new(default_opts, :request => request_for(IE)) - expect(csp.value).to match "default-src" + it "removes http/s schemes from hosts" do + csp = ContentSecurityPolicy.new(default_src: %w(https://example.org)) + expect(csp.value).to eq("default-src example.org") end - context "Firefox" do - it "builds a csp header for firefox" do - csp = ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)) - expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;") - end + it "does not remove schemes from report-uri values" do + csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org)) + expect(csp.value).to eq("default-src https:; report-uri https://example.org") end - context "Chrome" do - it "builds a csp header for chrome" do - csp = ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)) - expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;") - end + it "removes nil from source lists" do + csp = ContentSecurityPolicy.new(default_src: ["https://example.org", nil]) + expect(csp.value).to eq("default-src example.org") end - context "when using a nonce" do - it "adds a nonce and unsafe-inline to the script-src value when using chrome" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(CHROME), :controller => controller) - expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") - end - - it "adds a nonce and unsafe-inline to the script-src value when using firefox" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(FIREFOX), :controller => controller) - expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") - end - - it "adds a nonce and unsafe-inline to the script-src value when using opera" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(OPERA), :controller => controller) - expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") - end - - it "does not add a nonce and unsafe-inline to the script-src value when using Safari" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(SAFARI), :controller => controller) - expect(header.value).to include("script-src 'self' 'unsafe-inline'") - expect(header.value).not_to include("nonce") - end - - it "does not add a nonce and unsafe-inline to the script-src value when using IE" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(IE), :controller => controller) - expect(header.value).to include("script-src 'self' 'unsafe-inline'") - expect(header.value).not_to include("nonce") - end - - it "adds a nonce and unsafe-inline to the style-src value" do - header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "'self' nonce"), :request => request_for(CHROME), :controller => controller) - expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'") - end - - it "adds an identical nonce to the style and script-src directives" do - header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "'self' nonce", :script_src => "'self' nonce"), :request => request_for(CHROME), :controller => controller) - nonce = header.nonce - value = header.value - expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'") - expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'") - end - - it "does not add 'unsafe-inline' twice" do - header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce 'unsafe-inline'"), :request => request_for(CHROME), :controller => controller) - expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';") - end + it "deduplicates any source expressions" do + csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org)) + expect(csp.value).to eq("default-src example.org") end - context "when supplying additional http directive values" do - let(:options) { - default_opts.merge({ - :http_additions => { - :frame_src => "http:", - :img_src => "http:" - } - }) - } - - it "adds directive values for headers on http" do - csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME)) - expect(csp.value).to eq("default-src https:; frame-src http:; img-src https: data: http:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;") + context "browser sniffing" do + let (:complex_opts) do + ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = %w('self') } + .merge(block_all_mixed_content: true, reflected_xss: "block") + .merge(script_src: %w('self'), script_nonce: 123456) end - it "does not add the directive values if requesting https" do - csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME, '/', :ssl => true)) - expect(csp.value).not_to match(/http:/) + it "does not filter any directives for Chrome" do + policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'") end - it "does not add the directive values if requesting https" do - csp = ContentSecurityPolicy.new(options, :ua => "Chrome", :ssl => true) - expect(csp.value).not_to match(/http:/) + it "does not filter any directives for Opera" do + policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'") end - end - describe "class methods" do - let(:ua) { CHROME } - let(:env) do - double.tap do |env| - allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua) - end - end - let(:request) do - double( - :ssl? => true, - :url => 'https://example.com', - :env => env - ) - end - - describe ".add_to_env" do - let(:controller) { double } - let(:config) { {:default_src => "'self'"} } - let(:options) { {:controller => controller} } - - it "adds metadata to env" do - metadata = { - :config => config, - :options => options - } - expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options) - expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata) - ContentSecurityPolicy.add_to_env(request, controller, config) - end + it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do + policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox]) + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'") end - describe ".options_from_request" do - it "extracts options from request" do - options = ContentSecurityPolicy.options_from_request(request) - expect(options).to eql({ - :ua => ua, - :ssl => true, - :request_uri => 'https://example.com' - }) - end + it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do + policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) + expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'") end end end diff --git a/spec/lib/secure_headers/headers/public_key_pins_spec.rb b/spec/lib/secure_headers/headers/public_key_pins_spec.rb index 4e90e085..65dec5ea 100644 --- a/spec/lib/secure_headers/headers/public_key_pins_spec.rb +++ b/spec/lib/secure_headers/headers/public_key_pins_spec.rb @@ -2,35 +2,35 @@ module SecureHeaders describe PublicKeyPins do - specify{ expect(PublicKeyPins.new(:max_age => 1234).name).to eq("Public-Key-Pins-Report-Only") } - specify{ expect(PublicKeyPins.new(:max_age => 1234, :enforce => true).name).to eq("Public-Key-Pins") } + specify { expect(PublicKeyPins.new(max_age: 1234, :report_only => true).name).to eq("Public-Key-Pins-Report-Only") } + specify { expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins") } - specify { expect(PublicKeyPins.new({:max_age => 1234}).value).to eq("max-age=1234")} - specify { expect(PublicKeyPins.new(:max_age => 1234).value).to eq("max-age=1234")} - specify { - config = {:max_age => 1234, :pins => [{:sha256 => 'base64encodedpin1'}, {:sha256 => 'base64encodedpin2'}]} + specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } + specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } + specify do + config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin1' }, { sha256: 'base64encodedpin2' }] } header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\"" expect(PublicKeyPins.new(config).value).to eq(header_value) - } + end context "with an invalid configuration" do it "raises an exception when max-age is not provided" do - expect { - PublicKeyPins.new(:foo => 'bar') - }.to raise_error(PublicKeyPinsBuildError) + expect do + PublicKeyPins.validate_config!(foo: 'bar') + end.to raise_error(PublicKeyPinsConfigError) end it "raises an exception with an invalid max-age" do - expect { - PublicKeyPins.new(:max_age => 'abc123') - }.to raise_error(PublicKeyPinsBuildError) + expect do + PublicKeyPins.validate_config!(max_age: 'abc123') + end.to raise_error(PublicKeyPinsConfigError) end it 'raises an exception with less than 2 pins' do - expect { - config = {:max_age => 1234, :pins => [{:sha256 => 'base64encodedpin'}]} - PublicKeyPins.new(config) - }.to raise_error(PublicKeyPinsBuildError) + expect do + config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin' }] } + PublicKeyPins.validate_config!(config) + end.to raise_error(PublicKeyPinsConfigError) end end end diff --git a/spec/lib/secure_headers/headers/strict_transport_security_spec.rb b/spec/lib/secure_headers/headers/strict_transport_security_spec.rb index a58b1f16..9499b685 100644 --- a/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +++ b/spec/lib/secure_headers/headers/strict_transport_security_spec.rb @@ -2,60 +2,28 @@ module SecureHeaders describe StrictTransportSecurity do - specify{ expect(StrictTransportSecurity.new.name).to eq("Strict-Transport-Security") } - describe "#value" do - specify { expect(StrictTransportSecurity.new.value).to eq(StrictTransportSecurity::Constants::DEFAULT_VALUE)} - specify { expect(StrictTransportSecurity.new("max-age=1234").value).to eq("max-age=1234")} - specify { expect(StrictTransportSecurity.new(:max_age => '1234').value).to eq("max-age=1234")} - specify { expect(StrictTransportSecurity.new(:max_age => 1234).value).to eq("max-age=1234")} - specify { expect(StrictTransportSecurity.new(:max_age => HSTS_MAX_AGE, :include_subdomains => true).value).to eq("max-age=#{HSTS_MAX_AGE}; includeSubdomains")} - specify { expect(StrictTransportSecurity.new(:max_age => HSTS_MAX_AGE, :include_subdomains => true, :preload => true).value).to eq("max-age=#{HSTS_MAX_AGE}; includeSubdomains; preload")} + specify { expect(StrictTransportSecurity.make_header).to eq([StrictTransportSecurity::HEADER_NAME, StrictTransportSecurity::DEFAULT_VALUE]) } + specify { expect(StrictTransportSecurity.make_header("max-age=1234")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234"]) } context "with an invalid configuration" do - context "with a hash argument" do - it "should allow string values for max-age" do - expect { - StrictTransportSecurity.new(:max_age => '1234') - }.not_to raise_error - end - - it "should allow integer values for max-age" do - expect { - StrictTransportSecurity.new(:max_age => 1234) - }.not_to raise_error - end - - it "raises an exception with an invalid max-age" do - expect { - StrictTransportSecurity.new(:max_age => 'abc123') - }.to raise_error(STSBuildError) - end - - it "raises an exception if max-age is not supplied" do - expect { - StrictTransportSecurity.new(:includeSubdomains => true) - }.to raise_error(STSBuildError) - end - end - context "with a string argument" do it "raises an exception with an invalid max-age" do - expect { - StrictTransportSecurity.new('max-age=abc123') - }.to raise_error(STSBuildError) + expect do + StrictTransportSecurity.validate_config!('max-age=abc123') + end.to raise_error(STSConfigError) end it "raises an exception if max-age is not supplied" do - expect { - StrictTransportSecurity.new('includeSubdomains') - }.to raise_error(STSBuildError) + expect do + StrictTransportSecurity.validate_config!('includeSubdomains') + end.to raise_error(STSConfigError) end it "raises an exception with an invalid format" do - expect { - StrictTransportSecurity.new('max-age=123includeSubdomains') - }.to raise_error(STSBuildError) + expect do + StrictTransportSecurity.validate_config!('max-age=123includeSubdomains') + end.to raise_error(STSConfigError) end end end diff --git a/spec/lib/secure_headers/headers/x_content_type_options_spec.rb b/spec/lib/secure_headers/headers/x_content_type_options_spec.rb index 62f49f73..face9f84 100644 --- a/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +++ b/spec/lib/secure_headers/headers/x_content_type_options_spec.rb @@ -1,33 +1,28 @@ +require 'spec_helper' + module SecureHeaders describe XContentTypeOptions do - specify{ expect(XContentTypeOptions.new.name).to eq("X-Content-Type-Options") } - describe "#value" do - specify { expect(XContentTypeOptions.new.value).to eq(XContentTypeOptions::Constants::DEFAULT_VALUE)} - specify { expect(XContentTypeOptions.new("nosniff").value).to eq("nosniff")} - specify { expect(XContentTypeOptions.new(:value => 'nosniff').value).to eq("nosniff")} + specify { expect(XContentTypeOptions.make_header).to eq([XContentTypeOptions::HEADER_NAME, XContentTypeOptions::DEFAULT_VALUE]) } + specify { expect(XContentTypeOptions.make_header("nosniff")).to eq([XContentTypeOptions::HEADER_NAME, "nosniff"]) } context "invalid configuration values" do it "accepts nosniff" do - expect { - XContentTypeOptions.new("nosniff") - }.not_to raise_error - - expect { - XContentTypeOptions.new(:value => "nosniff") - }.not_to raise_error + expect do + XContentTypeOptions.validate_config!("nosniff") + end.not_to raise_error end it "accepts nil" do - expect { - XContentTypeOptions.new - }.not_to raise_error + expect do + XContentTypeOptions.validate_config!(nil) + end.not_to raise_error end it "doesn't accept anything besides no-sniff" do - expect { - XContentTypeOptions.new("donkey") - }.to raise_error(XContentTypeOptionsBuildError) + expect do + XContentTypeOptions.validate_config!("donkey") + end.to raise_error(XContentTypeOptionsConfigError) end end end diff --git a/spec/lib/secure_headers/headers/x_download_options_spec.rb b/spec/lib/secure_headers/headers/x_download_options_spec.rb index f869afca..798e4287 100644 --- a/spec/lib/secure_headers/headers/x_download_options_spec.rb +++ b/spec/lib/secure_headers/headers/x_download_options_spec.rb @@ -1,31 +1,27 @@ +require 'spec_helper' + module SecureHeaders describe XDownloadOptions do - specify { expect(XDownloadOptions.new.name).to eq(XDO_HEADER_NAME)} - specify { expect(XDownloadOptions.new.value).to eq("noopen")} - specify { expect(XDownloadOptions.new('noopen').value).to eq('noopen')} - specify { expect(XDownloadOptions.new(:value => 'noopen').value).to eq('noopen') } + specify { expect(XDownloadOptions.make_header).to eq([XDownloadOptions::HEADER_NAME, XDownloadOptions::DEFAULT_VALUE]) } + specify { expect(XDownloadOptions.make_header('noopen')).to eq([XDownloadOptions::HEADER_NAME, 'noopen']) } context "invalid configuration values" do it "accepts noopen" do - expect { - XDownloadOptions.new("noopen") - }.not_to raise_error - - expect { - XDownloadOptions.new(:value => "noopen") - }.not_to raise_error + expect do + XDownloadOptions.validate_config!("noopen") + end.not_to raise_error end it "accepts nil" do - expect { - XDownloadOptions.new - }.not_to raise_error + expect do + XDownloadOptions.validate_config!(nil) + end.not_to raise_error end it "doesn't accept anything besides noopen" do - expect { - XDownloadOptions.new("open") - }.to raise_error(XDOBuildError) + expect do + XDownloadOptions.validate_config!("open") + end.to raise_error(XDOConfigError) end end end diff --git a/spec/lib/secure_headers/headers/x_frame_options_spec.rb b/spec/lib/secure_headers/headers/x_frame_options_spec.rb index cf02a5ec..1c050d94 100644 --- a/spec/lib/secure_headers/headers/x_frame_options_spec.rb +++ b/spec/lib/secure_headers/headers/x_frame_options_spec.rb @@ -2,34 +2,32 @@ module SecureHeaders describe XFrameOptions do - specify{ expect(XFrameOptions.new.name).to eq("X-Frame-Options") } - describe "#value" do - specify { expect(XFrameOptions.new.value).to eq(XFrameOptions::Constants::DEFAULT_VALUE)} - specify { expect(XFrameOptions.new("SAMEORIGIN").value).to eq("SAMEORIGIN")} - specify { expect(XFrameOptions.new(:value => 'DENY').value).to eq("DENY")} + specify { expect(XFrameOptions.make_header).to eq([XFrameOptions::HEADER_NAME, XFrameOptions::DEFAULT_VALUE]) } + specify { expect(XFrameOptions.make_header("DENY")).to eq([XFrameOptions::HEADER_NAME, "DENY"]) } context "with invalid configuration" do it "allows SAMEORIGIN" do - expect { - XFrameOptions.new("SAMEORIGIN").value - }.not_to raise_error + expect do + XFrameOptions.validate_config!("SAMEORIGIN") + end.not_to raise_error end it "allows DENY" do - expect { - XFrameOptions.new("DENY").value - }.not_to raise_error end + expect do + XFrameOptions.validate_config!("DENY") + end.not_to raise_error + end it "allows ALLOW-FROM*" do - expect { - XFrameOptions.new("ALLOW-FROM: example.com").value - }.not_to raise_error + expect do + XFrameOptions.validate_config!("ALLOW-FROM: example.com") + end.not_to raise_error end it "does not allow garbage" do - expect { - XFrameOptions.new("I like turtles").value - }.to raise_error(XFOBuildError) + expect do + XFrameOptions.validate_config!("I like turtles") + end.to raise_error(XFOConfigError) end end end diff --git a/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb b/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb index fb981b74..86744fc2 100644 --- a/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +++ b/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb @@ -1,63 +1,46 @@ +require 'spec_helper' + module SecureHeaders describe XPermittedCrossDomainPolicies do - specify { expect(XPermittedCrossDomainPolicies.new.name).to eq(XPermittedCrossDomainPolicies::Constants::XPCDP_HEADER_NAME)} - specify { expect(XPermittedCrossDomainPolicies.new.value).to eq("none")} - specify { expect(XPermittedCrossDomainPolicies.new('master-only').value).to eq('master-only')} - specify { expect(XPermittedCrossDomainPolicies.new(:value => 'master-only').value).to eq('master-only') } + specify { expect(XPermittedCrossDomainPolicies.make_header).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, "none"]) } + specify { expect(XPermittedCrossDomainPolicies.make_header('master-only')).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, 'master-only']) } context "valid configuration values" do it "accepts 'all'" do - expect { - XPermittedCrossDomainPolicies.new("all") - }.not_to raise_error - - expect { - XPermittedCrossDomainPolicies.new(:value => "all") - }.not_to raise_error + expect do + XPermittedCrossDomainPolicies.validate_config!("all") + end.not_to raise_error end it "accepts 'by-ftp-filename'" do - expect { - XPermittedCrossDomainPolicies.new("by-ftp-filename") - }.not_to raise_error - - expect { - XPermittedCrossDomainPolicies.new(:value => "by-ftp-filename") - }.not_to raise_error + expect do + XPermittedCrossDomainPolicies.validate_config!("by-ftp-filename") + end.not_to raise_error end it "accepts 'by-content-type'" do - expect { - XPermittedCrossDomainPolicies.new("by-content-type") - }.not_to raise_error - - expect { - XPermittedCrossDomainPolicies.new(:value => "by-content-type") - }.not_to raise_error + expect do + XPermittedCrossDomainPolicies.validate_config!("by-content-type") + end.not_to raise_error end it "accepts 'master-only'" do - expect { - XPermittedCrossDomainPolicies.new("master-only") - }.not_to raise_error - - expect { - XPermittedCrossDomainPolicies.new(:value => "master-only") - }.not_to raise_error + expect do + XPermittedCrossDomainPolicies.validate_config!("master-only") + end.not_to raise_error end it "accepts nil" do - expect { - XPermittedCrossDomainPolicies.new - }.not_to raise_error + expect do + XPermittedCrossDomainPolicies.validate_config!(nil) + end.not_to raise_error end end context 'invlaid configuration values' do - it "doesn't accept invalid values" do - expect { - XPermittedCrossDomainPolicies.new("open") - }.to raise_error(XPCDPBuildError) + expect do + XPermittedCrossDomainPolicies.validate_config!("open") + end.to raise_error(XPCDPConfigError) end end end diff --git a/spec/lib/secure_headers/headers/x_xss_protection_spec.rb b/spec/lib/secure_headers/headers/x_xss_protection_spec.rb index 8c7b2de5..f5fbb8e8 100644 --- a/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +++ b/spec/lib/secure_headers/headers/x_xss_protection_spec.rb @@ -1,54 +1,46 @@ +require 'spec_helper' + module SecureHeaders describe XXssProtection do - specify { expect(XXssProtection.new.name).to eq(X_XSS_PROTECTION_HEADER_NAME)} - specify { expect(XXssProtection.new.value).to eq("1")} - specify { expect(XXssProtection.new("0").value).to eq("0")} - specify { expect(XXssProtection.new(:value => 1, :mode => 'block').value).to eq('1; mode=block') } - specify { expect(XXssProtection.new(:value => 1, :mode => 'block', :report_uri => 'https://www.secure.com/reports').value).to eq('1; mode=block; report=https://www.secure.com/reports') } + specify { expect(XXssProtection.make_header).to eq([XXssProtection::HEADER_NAME, XXssProtection::DEFAULT_VALUE]) } + specify { expect(XXssProtection.make_header("1; mode=block; report=https://www.secure.com/reports")).to eq([XXssProtection::HEADER_NAME, '1; mode=block; report=https://www.secure.com/reports']) } context "with invalid configuration" do it "should raise an error when providing a string that is not valid" do - expect { - XXssProtection.new("asdf") - }.to raise_error(XXssProtectionBuildError) + expect do + XXssProtection.validate_config!("asdf") + end.to raise_error(XXssProtectionConfigError) - expect { - XXssProtection.new("asdf; mode=donkey") - }.to raise_error(XXssProtectionBuildError) + expect do + XXssProtection.validate_config!("asdf; mode=donkey") + end.to raise_error(XXssProtectionConfigError) end context "when using a hash value" do it "should allow string values ('1' or '0' are the only valid strings)" do - expect { - XXssProtection.new(:value => '1') - }.not_to raise_error - end - - it "should allow integer values (1 or 0 are the only valid integers)" do - expect { - XXssProtection.new(:value => 1) - }.not_to raise_error + expect do + XXssProtection.validate_config!('1') + end.not_to raise_error end it "should raise an error if no value key is supplied" do - expect { - XXssProtection.new(:mode => 'block') - }.to raise_error(XXssProtectionBuildError) + expect do + XXssProtection.validate_config!("mode=block") + end.to raise_error(XXssProtectionConfigError) end it "should raise an error if an invalid key is supplied" do - expect { - XXssProtection.new(:value => 123) - }.to raise_error(XXssProtectionBuildError) + expect do + XXssProtection.validate_config!("123") + end.to raise_error(XXssProtectionConfigError) end it "should raise an error if mode != block" do - expect { - XXssProtection.new(:value => 1, :mode => "donkey") - }.to raise_error(XXssProtectionBuildError) + expect do + XXssProtection.validate_config!("1; mode=donkey") + end.to raise_error(XXssProtectionConfigError) end end - end end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb new file mode 100644 index 00000000..418f88f4 --- /dev/null +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -0,0 +1,40 @@ +require "spec_helper" + +module SecureHeaders + describe Middleware do + let(:app) { ->(env) { [200, env, "app"] } } + + let :middleware do + Middleware.new(app) + end + + before(:each) do + reset_config + Configuration.default do |config| + # use all default provided by the library + end + end + + it "sets the headers" do + _, env = middleware.call(Rack::MockRequest.env_for("https://looocalhost", {})) + expect_default_values(env) + end + + it "respects overrides" do + request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") + SecureHeaders.override_x_frame_options(request, "DENY") + _, env = middleware.call request.env + expect(env[XFrameOptions::HEADER_NAME]).to eq("DENY") + end + + it "uses named overrides" do + Configuration.override("my_custom_config") do |config| + config.csp[:script_src] = %w(example.org) + end + request = Rack::Request.new({}) + SecureHeaders.use_secure_headers_override(request, "my_custom_config") + _, env = middleware.call request.env + expect(env[CSP::HEADER_NAME]).to match("example.org") + end + end +end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 31378ed3..9e630f46 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -1,360 +1,240 @@ require 'spec_helper' -describe SecureHeaders do - subject {DummyClass.new} - let(:headers) {double} - let(:response) {double(:headers => headers)} - let(:max_age) {99} - let(:request) {double(:ssl? => true, :url => 'https://example.com')} - - before(:each) do - stub_user_agent(nil) - allow(headers).to receive(:[]) - allow(subject).to receive(:response).and_return(response) - allow(subject).to receive(:request).and_return(request) - end - - def stub_user_agent val - allow(request).to receive_message_chain(:env, :[]).and_return(val) - end - - def reset_config - ::SecureHeaders::Configuration.configure do |config| - config.hpkp = nil - config.hsts = nil - config.x_frame_options = nil - config.x_content_type_options = nil - config.x_xss_protection = nil - config.csp = nil - config.x_download_options = nil - config.x_permitted_cross_domain_policies = nil - end - end - - def set_security_headers(subject) - subject.set_csp_header - subject.set_hpkp_header - subject.set_hsts_header - subject.set_x_frame_options_header - subject.set_x_content_type_options_header - subject.set_x_xss_protection_header - subject.set_x_download_options_header - subject.set_x_permitted_cross_domain_policies_header - end - - describe "#set_header" do - it "accepts name/value pairs" do - should_assign_header("X-Hipster-Ipsum", "kombucha") - subject.send(:set_header, "X-Hipster-Ipsum", "kombucha") - end - - it "accepts header objects" do - should_assign_header("Strict-Transport-Security", SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE) - subject.send(:set_header, SecureHeaders::StrictTransportSecurity.new) - end - end +module SecureHeaders + describe SecureHeaders do + example_hpkp_config = { + max_age: 1_000_000, + include_subdomains: true, + report_uri: '//example.com/uri-directive', + pins: [ + { sha256: 'abc' }, + { sha256: '123' } + ] + } + + example_hpkp_config_value = %(max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains) + + before(:each) do + reset_config + @request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") + end + + it "raises a NotYetConfiguredError if default has not been set" do + expect do + SecureHeaders.header_hash_for(@request) + end.to raise_error(Configuration::NotYetConfiguredError) + end + + it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do + expect do + SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY) + end.to raise_error(Configuration::NotYetConfiguredError) + end + + describe "#header_hash_for" do + it "allows you to opt out of individual headers" do + Configuration.default + SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY) + hash = SecureHeaders.header_hash_for(@request) + expect(hash['Content-Security-Policy-Report-Only']).to be_nil + expect(hash['Content-Security-Policy']).to be_nil + end - describe "#set_security_headers" do - USER_AGENTS.each do |name, useragent| - it "sets all default headers for #{name} (smoke test)" do - stub_user_agent(useragent) - number_of_headers = 7 - expect(subject).to receive(:set_header).exactly(number_of_headers).times # a request for a given header - subject.set_csp_header - subject.set_x_frame_options_header - subject.set_hsts_header - subject.set_hpkp_header - subject.set_x_xss_protection_header - subject.set_x_content_type_options_header - subject.set_x_download_options_header - subject.set_x_permitted_cross_domain_policies_header + it "allows you to opt out entirely" do + Configuration.default + SecureHeaders.opt_out_of_all_protection(@request) + hash = SecureHeaders.header_hash_for(@request) + ALL_HEADER_CLASSES.each do |klass| + expect(hash[klass::CONFIG_KEY]).to be_nil + end end - end - it "does not set the X-Content-Type-Options header if disabled" do - stub_user_agent(USER_AGENTS[:ie]) - should_not_assign_header(X_CONTENT_TYPE_OPTIONS_HEADER_NAME) - subject.set_x_content_type_options_header(false) - end + it "allows you to override X-Frame-Options settings" do + Configuration.default + SecureHeaders.override_x_frame_options(@request, XFrameOptions::DENY) + hash = SecureHeaders.header_hash_for(@request) + expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY) + end - it "does not set the X-XSS-Protection header if disabled" do - should_not_assign_header(X_XSS_PROTECTION_HEADER_NAME) - subject.set_x_xss_protection_header(false) - end + it "allows you to override opting out" do + Configuration.default do |config| + config.x_frame_options = OPT_OUT + config.csp = OPT_OUT + end - it "does not set the X-Download-Options header if disabled" do - should_not_assign_header(XDO_HEADER_NAME) - subject.set_x_download_options_header(false) - end + SecureHeaders.override_x_frame_options(@request, XFrameOptions::SAMEORIGIN) + SecureHeaders.override_content_security_policy_directives(@request, default_src: %w(https:), script_src: %w('self')) - it "does not set the X-Frame-Options header if disabled" do - should_not_assign_header(XFO_HEADER_NAME) - subject.set_x_frame_options_header(false) - end + hash = SecureHeaders.header_hash_for(@request) + expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'") + expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN) + end - it "does not set the X-Permitted-Cross-Domain-Policies header if disabled" do - should_not_assign_header(XPCDP_HEADER_NAME) - subject.set_x_permitted_cross_domain_policies_header(false) - end + it "produces a hash of headers with default config" do + Configuration.default + hash = SecureHeaders.header_hash_for(@request) + expect_default_values(hash) + end - it "does not set the HSTS header if disabled" do - should_not_assign_header(HSTS_HEADER_NAME) - subject.set_hsts_header(false) - end + it "does not set the HSTS header if request is over HTTP" do + plaintext_request = Rack::Request.new({}) + Configuration.default do |config| + config.hsts = "max-age=123456" + end + expect(SecureHeaders.header_hash_for(plaintext_request)[StrictTransportSecurity::HEADER_NAME]).to be_nil + end - it "does not set the HSTS header if request is over HTTP" do - allow(subject).to receive_message_chain(:request, :ssl?).and_return(false) - should_not_assign_header(HSTS_HEADER_NAME) - subject.set_hsts_header({:include_subdomains => true}) - end + it "does not set the HPKP header if request is over HTTP" do + plaintext_request = Rack::Request.new({}) + Configuration.default do |config| + config.hpkp = example_hpkp_config + end - it "does not set the HPKP header if disabled" do - should_not_assign_header(HPKP_HEADER_NAME) - subject.set_hpkp_header - end + expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil + end - it "does not set the HPKP header if request is over HTTP" do - allow(subject).to receive_message_chain(:request, :ssl?).and_return(false) - should_not_assign_header(HPKP_HEADER_NAME) - subject.set_hpkp_header(:max_age => 1234) - end + context "content security policy" do + it "appends a value to csp directive" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w(mycdn.com 'unsafe-inline') + } + end + + SecureHeaders.append_content_security_policy_directives(@request, script_src: %w(anothercdn.com)) + hash = SecureHeaders.header_hash_for(@request) + expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") + end - it "does not set the CSP header if disabled" do - stub_user_agent(USER_AGENTS[:chrome]) - should_not_assign_header(HEADER_NAME) - subject.set_csp_header(false) - end + it "overrides individual directives" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + end + SecureHeaders.override_content_security_policy_directives(@request, default_src: %w('none')) + hash = SecureHeaders.header_hash_for(@request) + expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'") + end - it "saves the options to the env when using script hashes" do - opts = { - :default_src => "'self'", - :script_hash_middleware => true - } - stub_user_agent(USER_AGENTS[:chrome]) + it "overrides non-existant directives" do + Configuration.default + SecureHeaders.override_content_security_policy_directives(@request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) + hash = SecureHeaders.header_hash_for(@request) + expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:") + end - expect(SecureHeaders::ContentSecurityPolicy).to receive(:add_to_env) - subject.set_csp_header(opts) - end + it "does not append a nonce when the browser does not support it" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w(mycdn.com 'unsafe-inline'), + style_src: %w('self') + } + end + + request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5])) + nonce = SecureHeaders.content_security_policy_script_nonce(request) + hash = SecureHeaders.header_hash_for(request) + expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'") + end - context "when disabled by configuration settings" do - it "does not set any headers when disabled" do - ::SecureHeaders::Configuration.configure do |config| - config.hsts = false - config.hpkp = false - config.x_frame_options = false - config.x_content_type_options = false - config.x_xss_protection = false - config.csp = false - config.x_download_options = false - config.x_permitted_cross_domain_policies = false + it "appends a nonce to the script-src when used" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w(mycdn.com), + style_src: %w('self') + } + end + + request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome])) + nonce = SecureHeaders.content_security_policy_script_nonce(request) + + # simulate the nonce being used multiple times in a request: + SecureHeaders.content_security_policy_script_nonce(request) + SecureHeaders.content_security_policy_script_nonce(request) + SecureHeaders.content_security_policy_script_nonce(request) + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") end - expect(subject).not_to receive(:set_header) - set_security_headers(subject) - reset_config end end - end - - describe "SecureHeaders#header_hash" do - def expect_default_values(hash) - expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) - expect(hash[XDO_HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE) - expect(hash[HSTS_HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE) - expect(hash[X_XSS_PROTECTION_HEADER_NAME]).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE) - expect(hash[X_CONTENT_TYPE_OPTIONS_HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) - expect(hash[XPCDP_HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE) - end - it "produces a hash of headers given a hash as config" do - hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:"}) - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;") - expect_default_values(hash) - end - - it "produces a hash with a mix of config values, override values, and default values" do - ::SecureHeaders::Configuration.configure do |config| - config.hsts = { :max_age => '123456'} - config.hpkp = { - :enforce => true, - :max_age => 1000000, - :include_subdomains => true, - :report_uri => '//example.com/uri-directive', - :pins => [ - {:sha256 => 'abc'}, - {:sha256 => '123'} - ] - } + context "validation" do + it "validates your hsts config upon configuration" do + expect do + Configuration.default do |config| + config.hsts = 'lol' + end + end.to raise_error(STSConfigError) end - hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:"}) - ::SecureHeaders::Configuration.configure do |config| - config.hsts = nil - config.hpkp = nil + it "validates your csp config upon configuration" do + expect do + Configuration.default do |config| + config.csp = { CSP::DEFAULT_SRC => '123456' } + end + end.to raise_error(ContentSecurityPolicyConfigError) end - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;") - expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) - expect(hash[HSTS_HEADER_NAME]).to eq("max-age=123456") - expect(hash[HPKP_HEADER_NAME]).to eq(%{max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains}) - end - - it "produces a hash of headers with default config" do - hash = SecureHeaders::header_hash - expect(hash['Content-Security-Policy-Report-Only']).to eq(SecureHeaders::ContentSecurityPolicy::Constants::DEFAULT_CSP_HEADER) - expect_default_values(hash) - end - end - - describe "#set_x_frame_options_header" do - it "sets the X-Frame-Options header" do - should_assign_header(XFO_HEADER_NAME, SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE) - subject.set_x_frame_options_header - end - - it "allows a custom X-Frame-Options header" do - should_assign_header(XFO_HEADER_NAME, "DENY") - subject.set_x_frame_options_header(:value => 'DENY') - end - end - - describe "#set_x_download_options_header" do - it "sets the X-Download-Options header" do - should_assign_header(XDO_HEADER_NAME, SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE) - subject.set_x_download_options_header - end - - it "allows a custom X-Download-Options header" do - should_assign_header(XDO_HEADER_NAME, "noopen") - subject.set_x_download_options_header(:value => 'noopen') - end - end - - describe "#set_strict_transport_security" do - it "sets the Strict-Transport-Security header" do - should_assign_header(HSTS_HEADER_NAME, SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE) - subject.set_hsts_header - end - - it "allows you to specific a custom max-age value" do - should_assign_header(HSTS_HEADER_NAME, 'max-age=1234') - subject.set_hsts_header(:max_age => 1234) - end - - it "allows you to specify includeSubdomains" do - should_assign_header(HSTS_HEADER_NAME, "max-age=#{HSTS_MAX_AGE}; includeSubdomains") - subject.set_hsts_header(:max_age => HSTS_MAX_AGE, :include_subdomains => true) - end - - it "allows you to specify preload" do - should_assign_header(HSTS_HEADER_NAME, "max-age=#{HSTS_MAX_AGE}; includeSubdomains; preload") - subject.set_hsts_header(:max_age => HSTS_MAX_AGE, :include_subdomains => true, :preload => true) - end - end - - describe "#set_public_key_pins" do - it "sets the Public-Key-Pins header" do - should_assign_header(HPKP_HEADER_NAME + "-Report-Only", "max-age=1234") - subject.set_hpkp_header(:max_age => 1234) - end - - it "allows you to enforce public key pinning" do - should_assign_header(HPKP_HEADER_NAME, "max-age=1234") - subject.set_hpkp_header(:max_age => 1234, :enforce => true) - end - - it "allows you to specific a custom max-age value" do - should_assign_header(HPKP_HEADER_NAME + "-Report-Only", 'max-age=1234') - subject.set_hpkp_header(:max_age => 1234) - end - - it "allows you to specify includeSubdomains" do - should_assign_header(HPKP_HEADER_NAME, "max-age=1234; includeSubDomains") - subject.set_hpkp_header(:max_age => 1234, :include_subdomains => true, :enforce => true) - end - - it "allows you to specify a report-uri" do - should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com\"") - subject.set_hpkp_header(:max_age => 1234, :report_uri => "https://foobar.com", :enforce => true) - end - - it "allows you to specify a report-uri with app_name" do - should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com?enforce=true&app_name=my_app\"") - subject.set_hpkp_header(:max_age => 1234, :report_uri => "https://foobar.com", :app_name => "my_app", :tag_report_uri => true, :enforce => true) - end - end - - describe "#set_x_xss_protection" do - it "sets the X-XSS-Protection header" do - should_assign_header(X_XSS_PROTECTION_HEADER_NAME, SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE) - subject.set_x_xss_protection_header - end - - it "sets a custom X-XSS-Protection header" do - should_assign_header(X_XSS_PROTECTION_HEADER_NAME, '0') - subject.set_x_xss_protection_header("0") - end - - it "sets the block flag" do - should_assign_header(X_XSS_PROTECTION_HEADER_NAME, '1; mode=block') - subject.set_x_xss_protection_header(:mode => 'block', :value => 1) - end - end - - describe "#set_x_content_type_options" do - USER_AGENTS.each do |useragent| - context "when using #{useragent}" do - before(:each) do - stub_user_agent(USER_AGENTS[useragent]) - end - - it "sets the X-Content-Type-Options header" do - should_assign_header(X_CONTENT_TYPE_OPTIONS_HEADER_NAME, SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE) - subject.set_x_content_type_options_header - end + it "raises errors for unknown directives" do + expect do + Configuration.default do |config| + config.csp = { :made_up_directive => '123456' } + end + end.to raise_error(ContentSecurityPolicyConfigError) + end - it "lets you override X-Content-Type-Options" do - should_assign_header(X_CONTENT_TYPE_OPTIONS_HEADER_NAME, 'nosniff') - subject.set_x_content_type_options_header(:value => 'nosniff') - end + it "validates your xfo config upon configuration" do + expect do + Configuration.default do |config| + config.x_frame_options = "NOPE" + end + end.to raise_error(XFOConfigError) end - end - end - describe "#set_csp_header" do - context "when using Firefox" do - it "sets CSP headers" do - stub_user_agent(USER_AGENTS[:firefox]) - should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER) - subject.set_csp_header + it "validates your xcto config upon configuration" do + expect do + Configuration.default do |config| + config.x_content_type_options = "lol" + end + end.to raise_error(XContentTypeOptionsConfigError) end - end - context "when using Chrome" do - it "sets default CSP header" do - stub_user_agent(USER_AGENTS[:chrome]) - should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER) - subject.set_csp_header + it "validates your x_xss config upon configuration" do + expect do + Configuration.default do |config| + config.x_xss_protection = "lol" + end + end.to raise_error(XXssProtectionConfigError) end - end - context "when using a browser besides chrome/firefox" do - it "sets the CSP header" do - stub_user_agent(USER_AGENTS[:opera]) - should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER) - subject.set_csp_header + it "validates your xdo config upon configuration" do + expect do + Configuration.default do |config| + config.x_download_options = "lol" + end + end.to raise_error(XDOConfigError) end - end - end - describe "#set_x_permitted_cross_domain_policies_header" do - it "sets the X-Permitted-Cross-Domain-Policies header" do - should_assign_header(XPCDP_HEADER_NAME, SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE) - subject.set_x_permitted_cross_domain_policies_header - end + it "validates your x_permitted_cross_domain_policies config upon configuration" do + expect do + Configuration.default do |config| + config.x_permitted_cross_domain_policies = "lol" + end + end.to raise_error(XPCDPConfigError) + end - it "allows a custom X-Permitted-Cross-Domain-Policies header" do - should_assign_header(XPCDP_HEADER_NAME, "master-only") - subject.set_x_permitted_cross_domain_policies_header(:value => 'master-only') + it "validates your hpkp config upon configuration" do + expect do + Configuration.default do |config| + config.hpkp = "lol" + end + end.to raise_error(PublicKeyPinsConfigError) + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0e150518..4520396b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,44 +1,44 @@ require 'rubygems' require 'rspec' +require 'rack' +require 'pry-nav' require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers') -begin - require 'coveralls' - Coveralls.wear! -rescue LoadError - # damn you 1.8.7 -end - -include ::SecureHeaders::PublicKeyPins::Constants -include ::SecureHeaders::StrictTransportSecurity::Constants -include ::SecureHeaders::ContentSecurityPolicy::Constants -include ::SecureHeaders::XFrameOptions::Constants -include ::SecureHeaders::XXssProtection::Constants -include ::SecureHeaders::XContentTypeOptions::Constants -include ::SecureHeaders::XDownloadOptions::Constants -include ::SecureHeaders::XPermittedCrossDomainPolicies::Constants +ENV["RAILS_ENV"] = "test" USER_AGENTS = { - :firefox => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1', - :chrome => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5', - :ie => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)', - :opera => 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00', - :ios5 => "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", - :ios6 => "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25", - :safari5 => "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3", - :safari5_1 => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", - :safari6 => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1" + firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1', + chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5', + ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)', + opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00', + ios5: "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", + ios6: "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25", + safari5: "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3", + safari5_1: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", + safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1" } -class DummyClass - include ::SecureHeaders +def expect_default_values(hash) + expect(hash[SecureHeaders::CSP::HEADER_NAME]).to eq(SecureHeaders::CSP::DEFAULT_VALUE) + expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) + expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) + expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) + expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE) + expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE) + expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE) end -def should_assign_header name, value - expect(response.headers).to receive(:[]=).with(name, value) +module SecureHeaders + class Configuration + class << self + def clear_configurations + @configurations = nil + end + end + end end -def should_not_assign_header name - expect(response.headers).not_to receive(:[]=).with(name, anything) +def reset_config + SecureHeaders::Configuration.clear_configurations end From 4170482646b8512976a834d08117336bf0281043 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 4 Nov 2015 15:45:04 -1000 Subject: [PATCH 078/636] only attempt to build directives that are configured --- lib/secure_headers/headers/content_security_policy.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index ea458ae5..1a7bbb2c 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -294,7 +294,6 @@ def name ## # Return the value of the CSP header def value - return @config if @config.is_a?(String) @value ||= if @config build_value else @@ -321,7 +320,7 @@ def build_value else build_directive(directive_name) end - end.compact.join("; ") + end.join("; ") end # Private: builds a string that represents one directive in a minified form. @@ -333,9 +332,7 @@ def build_value # # Returns a string representing a directive. def build_directive(directive_name) - source_list = @config[directive_name] - return unless source_list - source_list.compact! + source_list = @config[directive_name].compact value = if source_list.include?(STAR) # Discard trailing entries since * accomplishes the same. @@ -400,8 +397,8 @@ def append_nonce(source_list, nonce) # starting with default-src and ending with report-uri. def directives [DEFAULT_SRC, - BODY_DIRECTIVES.select { |key| supported_directives.include?(key) }.select { |directive| @config.key?(directive) }, - REPORT_URI].flatten + BODY_DIRECTIVES.select { |key| supported_directives.include?(key) }, + REPORT_URI].flatten.select { |directive| @config.key?(directive) } end # Private: Remove scheme from source expressions. From b4f672b6fd009381bc39c6034fc78804d24e2f34 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 4 Nov 2015 15:46:51 -1000 Subject: [PATCH 079/636] remove unused constant --- lib/secure_headers/headers/content_security_policy.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 1a7bbb2c..85ceb4bb 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -19,15 +19,6 @@ class ContentSecurityPolicy UNSAFE_INLINE = "'unsafe-inline'".freeze UNSAFE_EVAL = "'unsafe-eval'".freeze - SOURCE_VALUES = [ - STAR, - DATA_PROTOCOL, - SELF, - NONE, - UNSAFE_EVAL, - UNSAFE_INLINE - ].freeze - # leftover deprecated values that will be in common use upon upgrading. DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze From b0991eed620eacf4ddd5e94d1b788885bb3b1d60 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 4 Nov 2015 15:48:12 -1000 Subject: [PATCH 080/636] cleanup --- lib/secure_headers.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index fd4458d9..950014cc 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -16,9 +16,8 @@ # or ":optout_of_protection" as a config value to disable a given header module SecureHeaders OPT_OUT = :opt_out_of_protection - SCRIPT_HASH_CONFIG_FILE = "config/script_hashes.yml".freeze - SECURE_HEADERS_CONFIG = "secure_headers".freeze - NONCE_KEY = "content_security_policy_nonce".freeze + SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze + NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze HTTPS = "https".freeze CSP = ContentSecurityPolicy @@ -57,6 +56,8 @@ def opt_out_of_all_protection(request) # Public: override a given set of directives for the current request. If a # value already exists for a given directive, it will be overridden. # + # If CSP was previously OPT_OUT, a new blank policy is used. + # # additions - a hash containing directives. e.g. # :script_src => %w(another-host.com) def override_content_security_policy_directives(request, additions) From ba05040ee0723939a6317e2c9ec1e626cb0f1cfa Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 4 Nov 2015 15:51:40 -1000 Subject: [PATCH 081/636] remove method that was only used once --- lib/secure_headers.rb | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 950014cc..aa4b53f4 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -155,14 +155,6 @@ def content_security_policy_nonce(request, script_or_style) request.env[NONCE_KEY] end - # Private: retrieve the configuration being used for the current request. - # - # Retrives the default, overridden, or custom configuration for the given - # request. - def secure_headers_request_config(request) - request.env[SECURE_HEADERS_CONFIG] - end - # Private: convenience method for specifying which configuration object should # be used for this request. # @@ -225,7 +217,7 @@ def use_cached_headers(default_headers, request) # Checks to see if a named override is used for this request, then # Falls back to the global config def config_for(request) - secure_headers_request_config(request) || + request.env[SECURE_HEADERS_CONFIG] || Configuration.get(Configuration::DEFAULT_CONFIG) end From 97b56b368466f62eea4a722eb4226edd27be011e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 4 Nov 2015 15:52:39 -1000 Subject: [PATCH 082/636] reorder methods --- lib/secure_headers.rb | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index aa4b53f4..09467b3d 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -39,20 +39,6 @@ module SecureHeaders (ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze class << self - # Public: opts out of setting a given header by creating a temporary config - # and setting the given headers config to OPT_OUT. - def opt_out_of_header(request, header_key) - config = config_for(request).dup - config.send("#{header_key}=", OPT_OUT) - override_secure_headers_request_config(request, config) - end - - # Public: opts out of setting all headers by telling secure_headers to use - # the NOOP configuration. - def opt_out_of_all_protection(request) - use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION) - end - # Public: override a given set of directives for the current request. If a # value already exists for a given directive, it will be overridden. # @@ -92,6 +78,20 @@ def override_x_frame_options(request, value) override_secure_headers_request_config(request, default_config) end + # Public: opts out of setting a given header by creating a temporary config + # and setting the given headers config to OPT_OUT. + def opt_out_of_header(request, header_key) + config = config_for(request).dup + config.send("#{header_key}=", OPT_OUT) + override_secure_headers_request_config(request, config) + end + + # Public: opts out of setting all headers by telling secure_headers to use + # the NOOP configuration. + def opt_out_of_all_protection(request) + use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION) + end + # Public: Builds the hash of headers that should be applied base on the # request. # @@ -136,7 +136,7 @@ def content_security_policy_script_nonce(request) # Public: gets or creates a nonce for CSP. # - # The nonce will be added to script_src + # The nonce will be added to style_src # # Returns the nonce def content_security_policy_style_nonce(request) From da60685744e219a6490b8220bd7b2a55dcd36624 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 5 Nov 2015 07:50:18 -1000 Subject: [PATCH 083/636] documentation updates --- README.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 868a12af..aa1e694b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ SecureHeaders::Configuration.default do |config| config.csp = { :default_src => %w(https: 'self'), :report_only => false, - :frame_src => %w(https: http:.twimg.com http://itunes.apple.com), + :frame_src => %w(*.twimg.com itunes.apple.com), :connect_src => %w(wws:), :font_src => %w('self' data:), :frame_src => %w('self'), @@ -47,9 +47,10 @@ SecureHeaders::Configuration.default do |config| :report_uri => %w(https://example.com/uri-directive) } config.hpkp = { + :report_only => false, :max_age => 60.days.to_i, :include_subdomains => true, - :report_uri => "https//example.com/uri-directive", + :report_uri => "https://example.com/uri-directive", :pins => [ {:sha256 => "abc"}, {:sha256 => "123"} @@ -118,8 +119,6 @@ By default, a noop configuration is provided. No headers will be set when this d class MyController < ApplicationController def index SecureHeaders::opt_out_of_all_protection(request) - # or - use_secure_headers_override(SecureHeaders::Configuration::NOOP_CONFIGURATION) end end ``` @@ -178,16 +177,6 @@ Code | Result `append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com` `override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com` -#### Appending to an opted-out Policy - -If your policy is set to `SecureHeaders::OPT_OUT`, you will be appending to the default policy (`default-src https:`). This behaves the same way as the above example using the default configuration and will set the header based on the result. - -```ruby -::SecureHeaders::Configuration.configure do |config| - config.csp = SecureHeaders::OPT_OUT - end - ``` - Code | Result ------------- | ------------- `append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src https:; script-src https: mycdn.com` @@ -217,9 +206,14 @@ You can use a view helper to automatically add nonces to script tags: ```erb <%= nonced_javascript_tag do %> - console.log("nonced!") +console.log("hai"); +<% end %> + +<%= nonced_style_tag do %> +body { + background-color: black; +} <% end %> -<%= nonced_javascript_tag("nonced without a block!") %> ``` becomes: @@ -228,6 +222,11 @@ becomes: + ``` #### Hash From e3ac264d9e40da400af9ab6bab3cc5621072627b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 10 Nov 2015 08:53:37 -1000 Subject: [PATCH 084/636] parameters to nonce functions should be optional --- lib/secure_headers/view_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index dd410b28..728ae664 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -4,7 +4,7 @@ module ViewHelpers # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns an html-safe style tag with the nonce attribute. - def nonced_style_tag(content_or_options, &block) + def nonced_style_tag(content_or_options = nil, &block) nonced_tag(:style, content_or_options, block) end @@ -12,7 +12,7 @@ def nonced_style_tag(content_or_options, &block) # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns an html-safe script tag with the nonce attribute. - def nonced_javascript_tag(content_or_options, &block) + def nonced_javascript_tag(content_or_options = nil, &block) nonced_tag(:script, content_or_options, block) end From 68a7498099cf3b64476d62d7a01f132da7d0ccbe Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 10 Nov 2015 08:56:33 -1000 Subject: [PATCH 085/636] Keep unsafe-* around when a * is provided. e.g. default-src * 'unsafe-inline' 'unsafe-eval' host1.com http: https: can be reduced to: default-src * 'unsafe-inline' 'unsafe-eval' Previously, the unsafe-* values were removed but * does not cover the unsafe-* values. --- lib/secure_headers/headers/content_security_policy.rb | 10 ++++++++-- .../headers/content_security_policy_spec.rb | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 85ceb4bb..752758bc 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -138,6 +138,12 @@ class ContentSecurityPolicy STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) HTTP_SCHEME_REGEX = %r{\Ahttps?://} + WILDCARD_SOURCES = [ + UNSAFE_EVAL, + UNSAFE_INLINE, + STAR + ] + class << self # Public: generate a header name, value array that is user-agent-aware. # @@ -326,8 +332,8 @@ def build_directive(directive_name) source_list = @config[directive_name].compact value = if source_list.include?(STAR) - # Discard trailing entries since * accomplishes the same. - STAR + # Discard trailing entries (excluding unsafe-*) since * accomplishes the same. + source_list.select { |value| WILDCARD_SOURCES.include?(value) } else populate_nonces(directive_name, source_list) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index b2b784f4..64ff3584 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -109,9 +109,9 @@ module SecureHeaders expect(csp.value).not_to include("'none'") end - it "discards any other source expressions when * is present" do - csp = ContentSecurityPolicy.new(default_src: %w(* http: https: example.org)) - expect(csp.value).to eq("default-src *") + it "discards source expressions besides unsafe-* expressions when * is present" do + csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org)) + expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval'") end it "minifies source expressions based on overlapping wildcards" do From fb81baf1c9c24a96ea966ff50c9b228f14749034 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 10 Nov 2015 10:08:57 -1000 Subject: [PATCH 086/636] version lock tins --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index b227a920..2ab5cbea 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" gemspec group :test do + gem "tins", "~> 1.6" # 1.7 requires ruby 2.0 gem "pry-nav" gem "rack" gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] From 0d499f494ed762a7ceee80c465911846078a3c62 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 10 Nov 2015 10:58:04 -1000 Subject: [PATCH 087/636] actually use pessimistic version locking --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 2ab5cbea..f850d127 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gemspec group :test do - gem "tins", "~> 1.6" # 1.7 requires ruby 2.0 + gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0 gem "pry-nav" gem "rack" gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] From 5481cba6fa8d8964aba76d0d683d6456f5e7a140 Mon Sep 17 00:00:00 2001 From: Kevin Goslar Date: Mon, 16 Nov 2015 17:51:03 -0800 Subject: [PATCH 088/636] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4f5d0c5..6532fc6d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The gem will automatically apply several headers that are related to security. - X-Content-Type-Options - [Prevent content type sniffing](http://msdn.microsoft.com/en-us/library/ie/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) -- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinnning Specification](https://tools.ietf.org/html/rfc7469) +- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) ## Usage From 64908d5377665acb9d65f1731b57a7bafeb71fc2 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 3 Dec 2015 13:12:41 -1000 Subject: [PATCH 089/636] Fix issue with opting out of headers when using header_hash method --- lib/secure_headers.rb | 12 +++++++----- spec/lib/secure_headers_spec.rb | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 05cf1922..9f51f53b 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -52,21 +52,23 @@ def append_features(base) def header_hash(options = nil) ALL_HEADER_CLASSES.inject({}) do |memo, klass| - config = if options.is_a?(Hash) && options[klass::Constants::CONFIG_KEY] + # must use !options[key].nil? because 'false' represents opting out, nil + # represents use global default. + config = if options.is_a?(Hash) && !options[klass::Constants::CONFIG_KEY].nil? options[klass::Constants::CONFIG_KEY] else ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY) end unless klass == SecureHeaders::PublicKeyPins && !config.is_a?(Hash) - header = get_a_header(klass::Constants::CONFIG_KEY, klass, config) - memo[header.name] = header.value + header = get_a_header(klass, config) + memo[header.name] = header.value if header end memo end end - def get_a_header(name, klass, options) + def get_a_header(klass, options) return if options == false klass.new(options) end @@ -210,7 +212,7 @@ def secure_header_options_for(type, options) def set_a_header(name, klass, options=nil) options = secure_header_options_for(name, options) return if options == false - set_header(SecureHeaders::get_a_header(name, klass, options)) + set_header(SecureHeaders::get_a_header(klass, options)) end def set_header(name_or_header, value=nil) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 31378ed3..d5e3915e 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -8,6 +8,7 @@ let(:request) {double(:ssl? => true, :url => 'https://example.com')} before(:each) do + reset_config stub_user_agent(nil) allow(headers).to receive(:[]) allow(subject).to receive(:response).and_return(response) @@ -171,6 +172,23 @@ def expect_default_values(hash) expect_default_values(hash) end + it "allows opting out" do + hash = SecureHeaders::header_hash(:csp => false, :hpkp => false) + expect(hash['Content-Security-Policy-Report-Only']).to be_nil + expect(hash['Content-Security-Policy']).to be_nil + end + + it "allows opting out with config" do + ::SecureHeaders::Configuration.configure do |config| + config.hsts = false + config.csp = false + end + + hash = SecureHeaders::header_hash + expect(hash['Content-Security-Policy-Report-Only']).to be_nil + expect(hash['Content-Security-Policy']).to be_nil + end + it "produces a hash with a mix of config values, override values, and default values" do ::SecureHeaders::Configuration.configure do |config| config.hsts = { :max_age => '123456'} From 8e0e65424e90d86305c40d0c8b0c622d67e02823 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 3 Dec 2015 13:28:01 -1000 Subject: [PATCH 090/636] version bump --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index d6148c98..0eb330c0 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,3 @@ module SecureHeaders - VERSION = "2.4.3" + VERSION = "2.4.4" end From 27ec3921f76ff39a27d0786565e2ac3aa7ff1ec4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 3 Dec 2015 13:58:15 -1000 Subject: [PATCH 091/636] merge conflict fixes --- README.md | 2 +- lib/secure_headers/version.rb | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 lib/secure_headers/version.rb diff --git a/README.md b/README.md index 0f22e324..f0c0ae69 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The gem will automatically apply several headers that are related to security. - X-Content-Type-Options - [Prevent content type sniffing](http://msdn.microsoft.com/en-us/library/ie/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) -- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) +- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) `secure_headers` is a library with a global config, per request overrides, and rack milddleware that enables you customize your application settings. diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb deleted file mode 100644 index 0eb330c0..00000000 --- a/lib/secure_headers/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module SecureHeaders - VERSION = "2.4.4" -end From 9459ebe830e4b1dda2afd8515bbc37d3dd3287c0 Mon Sep 17 00:00:00 2001 From: Michael Noack Date: Fri, 11 Dec 2015 11:24:48 +1030 Subject: [PATCH 092/636] Add missing comma to correct hash syntax --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0c0ae69..7a9b6202 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ SecureHeaders::Configuration.default do |config| :form_action => %w('self' github.com), :frame_ancestors => %w('none'), :plugin_types => %w(application/x-shockwave-flash), - :block_all_mixed_content => true # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) + :block_all_mixed_content => true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) :report_uri => %w(https://example.com/uri-directive) } config.hpkp = { From 4af0071ee2643e78be9bb495a1e622bb9d700a78 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 10 Dec 2015 15:15:51 -1000 Subject: [PATCH 093/636] instructions on upgrading to 3.x --- README.md | 8 +++++--- upgrading-to-3-0.md | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 upgrading-to-3-0.md diff --git a/README.md b/README.md index f0c0ae69..f791bbd6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # SecureHeaders [![Build Status](https://travis-ci.org/twitter/secureheaders.png?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.png)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.png)](https://coveralls.io/r/twitter/secureheaders) +**The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. + The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) - HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797) @@ -14,7 +16,7 @@ The gem will automatically apply several headers that are related to security. ## Configuration -**Place the following in an initializer** if you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. +If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. All `nil` values will fallback to their default value. `SecureHeaders::OPT_OUT` will disable the header entirely. @@ -61,7 +63,7 @@ end ### rails 2 -`secure_headers` has a `railtie` that should automatically include the middleware. +For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 applications, an explicit statement is required to use the middleware component. ```ruby use SecureHeaders::Middleware @@ -144,7 +146,7 @@ class MyController < ApplicationController # Overrides the previously set source list, override 'none' values # Produces: default-src 'self'; script-src s3.amazaonaws.com; object-src 'self' - override_content_security_policy_directive(script_src: "s3.amazaonaws.com", object_src: %w('self')) + override_content_security_policy_directive(script_src: %w(s3.amazaonaws.com), object_src: %w('self')) # Global settings default to "sameorigin" override_x_frame_options("DENY") diff --git a/upgrading-to-3-0.md b/upgrading-to-3-0.md new file mode 100644 index 00000000..8b96f13c --- /dev/null +++ b/upgrading-to-3-0.md @@ -0,0 +1,35 @@ +`secure_headers` 3.0 is a near-complete rewrite. It includes breaking changes and removes a lot of features that were either leftover from the days when the CSP standard was not fully adopted or were just downright confusing. + +Changes +== + +| What | < = 2.x | >= 3.0 | +|----------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block | +| All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) | +| CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings | +| `self`/`none` source expressions | could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` | +| `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | +| Per-action configuration | override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) | + +Migrating to 3.x from <= 2.x +== + +1. Convert all headers except for CSP/HPKP using hashes to string values. The values are validated at runtime and will provide guidance on misconfigured headers. +1. Convert all instances of `self`/`none`/`eval`/`inline` to the corresponding values in the above table. +1. Convert all CSP space-delimited directives to an array of strings. + +Everything is terrible, why should I upgrade? +== + +`secure_headers` <= 2.x built every header per request using a series of automatically included `before_filters`. This is horribly inefficient because: + +1. `before_filters` are slow and adding 8 per request isn't great +1. We are rebuilding strings that may never change for every request +1. Errors in the request may mean that the headers never get set in the first place + +`secure_headers` 3.x sets headers in rack middleware that runs once per request and uses configuration values passed via `request.env`. This is much more efficient and somewhat guarantees that headers will always be set. **The values for the headers are cached and reused per request**. + +Also, there is a more flexible API for customizing content security policies / X-Frame-Options. In practice, none of the other headers need granular controls. One way of customizing headers per request is to use the helper methods. The only downside of this technique is that headers will be computed from scratch. + +See the [README](README.md) for more information. From e530450ec0e294106a3af7090c5bbdff39aaa9d7 Mon Sep 17 00:00:00 2001 From: Michael Noack Date: Fri, 11 Dec 2015 12:35:00 +1030 Subject: [PATCH 094/636] Use clean ruby hash syntax * Ruby 1.8 has not been supported for years --- README.md | 52 +++++++++---------- lib/secure_headers.rb | 4 +- .../headers/content_security_policy_spec.rb | 2 +- .../headers/public_key_pins_spec.rb | 2 +- spec/lib/secure_headers_spec.rb | 2 +- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 499ad2c5..bad429d6 100644 --- a/README.md +++ b/README.md @@ -29,33 +29,33 @@ SecureHeaders::Configuration.default do |config| config.x_download_options = "noopen" config.x_permitted_cross_domain_policies = "none" config.csp = { - :default_src => %w(https: 'self'), - :report_only => false, - :frame_src => %w(*.twimg.com itunes.apple.com), - :connect_src => %w(wws:), - :font_src => %w('self' data:), - :frame_src => %w('self'), - :img_src => %w(mycdn.com data:), - :media_src => %w(utoob.com), - :object_src => %w('self'), - :script_src => %w('self'), - :style_src => %w('unsafe-inline'), - :base_uri => %w('self'), - :child_src => %w('self'), - :form_action => %w('self' github.com), - :frame_ancestors => %w('none'), - :plugin_types => %w(application/x-shockwave-flash), - :block_all_mixed_content => true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) - :report_uri => %w(https://example.com/uri-directive) + default_src: %w(https: 'self'), + report_only: false, + frame_src: %w(*.twimg.com itunes.apple.com), + connect_src: %w(wws:), + font_src: %w('self' data:), + frame_src: %w('self'), + img_src: %w(mycdn.com data:), + media_src: %w(utoob.com), + object_src: %w('self'), + script_src: %w('self'), + style_src: %w('unsafe-inline'), + base_uri: %w('self'), + child_src: %w('self'), + form_action: %w('self' github.com), + frame_ancestors: %w('none'), + plugin_types: %w(application/x-shockwave-flash), + block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) + report_uri: %w(https://example.com/uri-directive) } config.hpkp = { - :report_only => false, - :max_age => 60.days.to_i, - :include_subdomains => true, - :report_uri => "https://example.com/uri-directive", - :pins => [ - {:sha256 => "abc"}, - {:sha256 => "123"} + report_only: false, + max_age: 60.days.to_i, + include_subdomains: true, + report_uri: "https://example.com/uri-directive", + pins: [ + {sha256: "abc"}, + {sha256: "123"} ] } end @@ -288,7 +288,7 @@ You can use SecureHeaders for Padrino applications as well: In your `Gemfile`: ```ruby - gem "secure_headers", :require => 'secure_headers' + gem "secure_headers", require: 'secure_headers' ``` then in your `app.rb` file you can: diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 09467b3d..10554c65 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -45,7 +45,7 @@ class << self # If CSP was previously OPT_OUT, a new blank policy is used. # # additions - a hash containing directives. e.g. - # :script_src => %w(another-host.com) + # script_src: %w(another-host.com) def override_content_security_policy_directives(request, additions) config = config_for(request).dup if config.csp == OPT_OUT @@ -60,7 +60,7 @@ def override_content_security_policy_directives(request, additions) # value. If a value exists for the given directive, the values will be combined. # # additions - a hash containing directives. e.g. - # :script_src => %w(another-host.com) + # script_src: %w(another-host.com) def append_content_security_policy_directives(request, additions) config = config_for(request).dup config.csp = CSP.combine_policies(config.csp, additions) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 64ff3584..4044565a 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -14,7 +14,7 @@ module SecureHeaders describe "#name" do context "when in report-only mode" do - specify { expect(ContentSecurityPolicy.new(default_opts.merge(:report_only => true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") } + specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") } end context "when in enforce mode" do diff --git a/spec/lib/secure_headers/headers/public_key_pins_spec.rb b/spec/lib/secure_headers/headers/public_key_pins_spec.rb index 65dec5ea..5f6d68f6 100644 --- a/spec/lib/secure_headers/headers/public_key_pins_spec.rb +++ b/spec/lib/secure_headers/headers/public_key_pins_spec.rb @@ -2,7 +2,7 @@ module SecureHeaders describe PublicKeyPins do - specify { expect(PublicKeyPins.new(max_age: 1234, :report_only => true).name).to eq("Public-Key-Pins-Report-Only") } + specify { expect(PublicKeyPins.new(max_age: 1234, report_only: true).name).to eq("Public-Key-Pins-Report-Only") } specify { expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins") } specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 9e630f46..f00e89fe 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -183,7 +183,7 @@ module SecureHeaders it "raises errors for unknown directives" do expect do Configuration.default do |config| - config.csp = { :made_up_directive => '123456' } + config.csp = { made_up_directive: '123456' } end end.to raise_error(ContentSecurityPolicyConfigError) end From 756fffadddae7894a03196b92d542e177d395891 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 11 Dec 2015 10:26:01 -1000 Subject: [PATCH 095/636] Handle nil values in directives Because configs can be environment-based and some values may not be configured for an environment, don't raise an exception during validation for nil values. --- lib/secure_headers/headers/content_security_policy.rb | 4 ++-- .../secure_headers/headers/content_security_policy_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 752758bc..45de71fc 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -243,11 +243,11 @@ def validate_directive!(key, value) # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_source_expression!(key, value) - # source expressions unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key) raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}") end - unless value.is_a?(Array) && value.all? { |v| v.is_a?(String) } + + unless value.is_a?(Array) && value.compact.all? { |v| v.is_a?(String) } raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings") end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 64ff3584..5cba05da 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -47,6 +47,12 @@ module SecureHeaders end.to raise_error(ContentSecurityPolicyConfigError) end + it "allows nil values" do + expect do + CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil]) + end.to_not raise_error + end + it "rejects unknown directives / config" do expect do CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve") From 53d318501a01637b492ddcd88f25d5b7fbd564cf Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 11 Dec 2015 10:28:52 -1000 Subject: [PATCH 096/636] Don't add a directive if the configuration is an empty array And because arrays are compacted, arrays of nils would have the same effect --- lib/secure_headers/headers/content_security_policy.rb | 3 ++- .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 45de71fc..2d3493f9 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -317,7 +317,7 @@ def build_value else build_directive(directive_name) end - end.join("; ") + end.compact.join("; ") end # Private: builds a string that represents one directive in a minified form. @@ -330,6 +330,7 @@ def build_value # Returns a string representing a directive. def build_directive(directive_name) source_list = @config[directive_name].compact + return if source_list.empty? value = if source_list.include?(STAR) # Discard trailing entries (excluding unsafe-*) since * accomplishes the same. diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 5cba05da..7a1e1fe0 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -143,6 +143,11 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org") end + it "does not add a directive if the value is an empty array (or all nil)" do + csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: [nil]) + expect(csp.value).to eq("default-src example.org") + end + it "deduplicates any source expressions" do csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org)) expect(csp.value).to eq("default-src example.org") From 782171ec9719cb9929baeb865eae55529d5598c7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 11 Dec 2015 10:30:06 -1000 Subject: [PATCH 097/636] * does not include protocol source values * will not allow data: blob: etc --- lib/secure_headers/headers/content_security_policy.rb | 5 ++++- .../secure_headers/headers/content_security_policy_spec.rb | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 2d3493f9..422e555a 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -13,6 +13,7 @@ class ContentSecurityPolicy REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze HEADER_NAMES = [HEADER_NAME, REPORT_ONLY] DATA_PROTOCOL = "data:".freeze + BLOB_PROTOCOL = "blob:".freeze SELF = "'self'".freeze NONE = "'none'".freeze STAR = "*".freeze @@ -141,7 +142,9 @@ class ContentSecurityPolicy WILDCARD_SOURCES = [ UNSAFE_EVAL, UNSAFE_INLINE, - STAR + STAR, + DATA_PROTOCOL, + BLOB_PROTOCOL ] class << self diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 7a1e1fe0..528e0ce8 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -115,9 +115,9 @@ module SecureHeaders expect(csp.value).not_to include("'none'") end - it "discards source expressions besides unsafe-* expressions when * is present" do - csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org)) - expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval'") + it "discards source expressions (besides unsafe-* and non-host source values) when * is present" do + csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org data: blob:)) + expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:") end it "minifies source expressions based on overlapping wildcards" do From 4458c1cf5cf850011f13b788bf0bc84fdb660d00 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 11 Dec 2015 12:50:13 -1000 Subject: [PATCH 098/636] Add reference to 2.x branch/README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bad429d6..7813088c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ **The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. +**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be maintained**. The documentation below only applies to the 2.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. + The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) - HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797) From 37f0ca53e74c10f933f4cc96af0c21edf42a89d7 Mon Sep 17 00:00:00 2001 From: Kevin Plattret Date: Tue, 15 Dec 2015 20:47:59 +0000 Subject: [PATCH 099/636] Specify "max-age" in HSTS string to prevent error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7813088c..d5bece34 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ All `nil` values will fallback to their default value. `SecureHeaders::OPT_OUT` ```ruby SecureHeaders::Configuration.default do |config| - config.hsts = 20.years.to_i.to_s + config.hsts = "max-age=#{20.years.to_i}" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" config.x_xss_protection = "1; mode=block" From a200392bac045daa478a24a23c43da21a2a0b3fe Mon Sep 17 00:00:00 2001 From: Sawyer Charles Date: Mon, 21 Dec 2015 18:07:25 -0600 Subject: [PATCH 100/636] added `to_h` to nonced_tag in view helper This will make it so you do not need to supply options for it to work, just setting it to nil or a blank string causes a error when you try to merge the nonce. --- lib/secure_headers/view_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 728ae664..a0618f5b 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -34,7 +34,7 @@ def content_security_policy_nonce(type) def nonced_tag(type, content_or_options, block) options = {} content = if block - options = content_or_options + options = content_or_options.to_h capture(&block) else content_or_options.html_safe # :'( From 0027d051aaed4c547721d6016c890990de4eec92 Mon Sep 17 00:00:00 2001 From: Sawyer Charles Date: Mon, 21 Dec 2015 21:49:04 -0600 Subject: [PATCH 101/636] Update view_helper.rb --- lib/secure_headers/view_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index a0618f5b..b5513d7d 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -4,7 +4,7 @@ module ViewHelpers # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns an html-safe style tag with the nonce attribute. - def nonced_style_tag(content_or_options = nil, &block) + def nonced_style_tag(content_or_options = {}, &block) nonced_tag(:style, content_or_options, block) end @@ -12,7 +12,7 @@ def nonced_style_tag(content_or_options = nil, &block) # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns an html-safe script tag with the nonce attribute. - def nonced_javascript_tag(content_or_options = nil, &block) + def nonced_javascript_tag(content_or_options = {}, &block) nonced_tag(:script, content_or_options, block) end @@ -34,7 +34,7 @@ def content_security_policy_nonce(type) def nonced_tag(type, content_or_options, block) options = {} content = if block - options = content_or_options.to_h + options = content_or_options capture(&block) else content_or_options.html_safe # :'( From f1d8b863cf4a6f764517dd764081e4000052af0a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 22 Dec 2015 08:39:13 -1000 Subject: [PATCH 102/636] version bump pre2 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 1d9b1078..2186e89c 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.0.0" + gem.version = "3.0.0.pre2" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 6212b268baa711af55c43e204b752740b1e6fb77 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 4 Jan 2016 10:40:41 -1000 Subject: [PATCH 103/636] Add 's' to override_content_security_policy_directive references --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5bece34..a93f7da0 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ class MyController < ApplicationController # Overrides the previously set source list, override 'none' values # Produces: default-src 'self'; script-src s3.amazaonaws.com; object-src 'self' - override_content_security_policy_directive(script_src: %w(s3.amazaonaws.com), object_src: %w('self')) + override_content_security_policy_directives(script_src: %w(s3.amazaonaws.com), object_src: %w('self')) # Global settings default to "sameorigin" override_x_frame_options("DENY") @@ -157,7 +157,7 @@ class MyController < ApplicationController The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object. * `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration. -* `override_content_security_policy_directive(hash)`: merges the hash into the app-wide configuration, overwriting any previous config +* `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config * `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value` ## Appending / overriding Content Security Policy From 8837a132348b082bddc0f637a5bdd14e263fccac Mon Sep 17 00:00:00 2001 From: Mike McCabe Date: Sun, 10 Jan 2016 11:03:43 -0500 Subject: [PATCH 104/636] Updating readme with correct sinatra config --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a93f7da0..415427e2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. -**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be maintained**. The documentation below only applies to the 2.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. +**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be maintained**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) @@ -269,7 +269,7 @@ require 'secure_headers' use SecureHeaders::Middleware -SecureHeaders::Configuration.configure do |config| +SecureHeaders::Configuration.default do |config| ... end From 44e29eed37e85a8f580ada3923c1e3010b028422 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 11 Jan 2016 08:09:07 -1000 Subject: [PATCH 105/636] Update more references to `configure` -> `default` Thanks to @mccabe615 for pointing the other out. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 415427e2..af7f6b2e 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ When manipulating content security policy, there are a few things to consider. T The value of `default_src` is joined with the addition. Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate: ```ruby -::SecureHeaders::Configuration.configure do |config| +::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self') } @@ -314,7 +314,7 @@ and in `config/boot.rb`: ```ruby def before_load - SecureHeaders::Configuration.configure do |config| + SecureHeaders::Configuration.default do |config| ... end end From 702bddf3c10fb50ed80fa9a33f6fc479946d4901 Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 14 Jan 2016 10:37:56 -0500 Subject: [PATCH 106/636] Remove duplicate configuration example fix for #205 --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index af7f6b2e..023978eb 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,9 @@ SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w(https: 'self'), report_only: false, - frame_src: %w(*.twimg.com itunes.apple.com), + frame_src: %w('self' *.twimg.com itunes.apple.com), connect_src: %w(wws:), font_src: %w('self' data:), - frame_src: %w('self'), img_src: %w(mycdn.com data:), media_src: %w(utoob.com), object_src: %w('self'), From bc1f5e49b2e28c3508ede179da3d6cc8c8ae5388 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 18 Jan 2016 06:49:15 -1000 Subject: [PATCH 107/636] Add reference to anotherhale/secure_headers --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 023978eb..619510e7 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,7 @@ end * ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) * Go - [secureheader](https://github.com/kr/secureheader) +* Elixir [secure_headers](https://github.com/anotherhale/secure_headers) ## License From 1ca78a3b1cc42f68af7fcee4e4b4c2e638f8b66a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 18 Jan 2016 06:51:34 -1000 Subject: [PATCH 108/636] Remove duplicated table content --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 619510e7..c22daee9 100644 --- a/README.md +++ b/README.md @@ -180,11 +180,6 @@ Code | Result `append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com` `override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com` -Code | Result -------------- | ------------- -`append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src https:; script-src https: mycdn.com` -`override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src https:; script-src mycdn.com` - #### Nonce script/style-nonce can be used to whitelist inline content. To do this, call the SecureHeaders::content_security_policy_nonce then set the nonce attributes on the various tags. From c215640ff99386680b2ade2167985493ec9aec22 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 18 Jan 2016 06:53:48 -1000 Subject: [PATCH 109/636] Clarify use of append/override of directives It's not that using these methods are not recommended, it's just important to note the minimal performance implications. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c22daee9..ba080284 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ end ## Per-action configuration -You can override the settings for a given action by producing a temporary override. This approach is not recommended because the header values will be computed per request. +You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request. ```ruby # Given a config of: From d6dc261509b0dffea4fb3474c7a5c39d6fb56a68 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 18 Jan 2016 06:59:01 -1000 Subject: [PATCH 110/636] `report_only` replaces `enforce` in 3.x --- upgrading-to-3-0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/upgrading-to-3-0.md b/upgrading-to-3-0.md index 8b96f13c..3d210a72 100644 --- a/upgrading-to-3-0.md +++ b/upgrading-to-3-0.md @@ -11,6 +11,7 @@ Changes | `self`/`none` source expressions | could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` | | `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | | Per-action configuration | override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) | +| CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` | Migrating to 3.x from <= 2.x == @@ -18,6 +19,7 @@ Migrating to 3.x from <= 2.x 1. Convert all headers except for CSP/HPKP using hashes to string values. The values are validated at runtime and will provide guidance on misconfigured headers. 1. Convert all instances of `self`/`none`/`eval`/`inline` to the corresponding values in the above table. 1. Convert all CSP space-delimited directives to an array of strings. +1. Convert all `enforce: true|false` to `report_only: true|false`. Everything is terrible, why should I upgrade? == From af15d837b9177766616835abf7eabf5db70fecf4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 8 Feb 2016 08:51:57 -1000 Subject: [PATCH 111/636] Add `gem install` line to README since the gem name does not match the repo name See https://github.com/twitter/secureheaders/issues/207 Sorry about that @maia --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba080284..1597c625 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# SecureHeaders [![Build Status](https://travis-ci.org/twitter/secureheaders.png?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.png)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.png)](https://coveralls.io/r/twitter/secureheaders) +# Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.png?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.png)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.png)](https://coveralls.io/r/twitter/secureheaders) + **The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. @@ -16,6 +17,10 @@ The gem will automatically apply several headers that are related to security. `secure_headers` is a library with a global config, per request overrides, and rack milddleware that enables you customize your application settings. +## Use + +`gem install secure_headers` + ## Configuration If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. From 645696fcc53a092a8fa93aef12e9a26c7b4e848b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 9 Feb 2016 13:27:06 -1000 Subject: [PATCH 112/636] If a policy modification is idempotent, don't dup the config `dup`ing the config will bust the header value cache unnecessarily when a policy modification has no effect --- lib/secure_headers.rb | 22 ++++++++++++------- .../headers/content_security_policy.rb | 19 ++++++++++------ .../headers/content_security_policy_spec.rb | 10 +++++++++ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 10554c65..0f48d92f 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -47,12 +47,15 @@ class << self # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) def override_content_security_policy_directives(request, additions) - config = config_for(request).dup - if config.csp == OPT_OUT - config.csp = {} + config = config_for(request) + unless CSP.idempotent_additions?(config.csp, additions) + config = config.dup + if config.csp == OPT_OUT + config.csp = {} + end + config.csp.merge!(additions) + override_secure_headers_request_config(request, config) end - config.csp.merge!(additions) - override_secure_headers_request_config(request, config) end # Public: appends source values to the current configuration. If no value @@ -62,9 +65,12 @@ def override_content_security_policy_directives(request, additions) # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) def append_content_security_policy_directives(request, additions) - config = config_for(request).dup - config.csp = CSP.combine_policies(config.csp, additions) - override_secure_headers_request_config(request, config) + config = config_for(request) + unless CSP.idempotent_additions?(config.csp, additions) + config = config.dup + config.csp = CSP.combine_policies(config.csp, additions) + override_secure_headers_request_config(request, config) + end end # Public: override X-Frame-Options settings for this request. diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 422e555a..d9dfc2f6 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -157,13 +157,7 @@ def make_header(config, user_agent) [header.name, header.value] end - # Public: Validates that the configuration has a valid type, or that it is a valid - # source expression. - # - # Private: validates that a source expression: - # 1. has a valid name - # 2. is an array of strings - # 3. does not contain any depreated, now invalid values (inline, eval, self, none) + # Public: Validates each source expression. # # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) @@ -179,6 +173,17 @@ def validate_config!(config) end end + # Public: determine if merging +additions+ will cause a change to the + # actual value of the config. + # + # e.g. config = { script_src: %w(example.org google.com)} and + # additions = { script_src: %w(google.com)} then idempotent_additions? would return + # because google.com is already in the config. + def idempotent_additions?(config, additions) + return false if config == OPT_OUT + config == combine_policies(config, additions) + end + # Public: combine the values from two different configs. # # original - the main config diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 67deb1d7..613869d5 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -109,6 +109,16 @@ module SecureHeaders end end + describe "#idempotent_additions?" do + specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false } + + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true } + end + describe "#value" do it "discards 'none' values if any other source expressions are present" do csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none'))) From 9978838ff4cedf46b68fd771af196251c8b6852c Mon Sep 17 00:00:00 2001 From: tomascassidy Date: Wed, 10 Feb 2016 14:49:07 +1100 Subject: [PATCH 113/636] Fix typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1597c625..201041ed 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The gem will automatically apply several headers that are related to security. - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) -`secure_headers` is a library with a global config, per request overrides, and rack milddleware that enables you customize your application settings. +`secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings. ## Use From db91ed3ff99f799bf059e9f452964d6a125b67ed Mon Sep 17 00:00:00 2001 From: ReadmeCritic Date: Thu, 11 Feb 2016 08:17:14 -0800 Subject: [PATCH 114/636] Update README URLs based on HTTP redirects --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 201041ed..059302a3 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ The gem will automatically apply several headers that are related to security. - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) - HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797) - X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options draft](https://tools.ietf.org/html/draft-ietf-websec-x-frame-options-02) -- X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](http://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx) -- X-Content-Type-Options - [Prevent content type sniffing](http://msdn.microsoft.com/en-us/library/ie/gg622941\(v=vs.85\).aspx) -- X-Download-Options - [Prevent file downloads opening](http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx) +- X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx) +- X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx) +- X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) @@ -321,8 +321,8 @@ end ## Similar libraries -* Rack [rack-secure_headers](https://github.com/harmoni/rack-secure_headers) -* Node.js (express) [helmet](https://github.com/evilpacket/helmet) and [hood](https://github.com/seanmonstar/hood) +* Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers) +* Node.js (express) [helmet](https://github.com/helmetjs/helmet) and [hood](https://github.com/seanmonstar/hood) * Node.js (hapi) [blankie](https://github.com/nlf/blankie) * J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines) * ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) From 4dffb231090324c805c577f32170bedcc99d128e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 11 Feb 2016 09:54:51 -1000 Subject: [PATCH 115/636] Add CSP config flag to preserve host source schemes Before this change, all schemes were stripped from host sources. This is because modern browsers know that on an HTTPS resource, only HTTPS is allowed but on HTTP resources, both HTTP and HTTPS are allowed. Safari on the other hand doesn't support this. In an ideal world, all local development would be over HTTPS and there would never be a need to serve/load HTTP resources. Adding :preserve_schemes (defaults to false) unfortunately also allows mixed content. i.e. If you are on an HTTPS resource, but you've whitelisted an HTTP resource you have allowed mixed content. --- lib/secure_headers/headers/content_security_policy.rb | 8 ++++++-- .../headers/content_security_policy_spec.rb | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 422e555a..2a260c84 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -275,7 +275,8 @@ def initialize(config = nil, user_agent = OTHER) else UserAgent.parse(user_agent) end - @report_only = !!@config[:report_only] + @report_only = @config[:report_only] + @preserve_schemes = @config[:preserve_schemes] @script_nonce = @config[:script_nonce] @style_nonce = @config[:style_nonce] end @@ -345,9 +346,12 @@ def build_directive(directive_name) source_list.reject! { |value| value == NONE } if source_list.length > 1 # remove schemes and dedup source expressions - source_list = strip_source_schemes(source_list) unless directive_name == REPORT_URI + unless directive_name == REPORT_URI || @preserve_schemes + source_list = strip_source_schemes(source_list) + end dedup_source_list(source_list).join(" ") end + [symbol_to_hyphen_case(directive_name), value].join(" ") end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 67deb1d7..2a957054 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -138,6 +138,11 @@ module SecureHeaders expect(csp.value).to eq("default-src https:; report-uri https://example.org") end + it "does not remove schemes when :preserve_schemes is true" do + csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :preserve_schemes => true) + expect(csp.value).to eq("default-src https://example.org") + end + it "removes nil from source lists" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org", nil]) expect(csp.value).to eq("default-src example.org") From 977c5d6d71a9a47ace5a2f85f06332590d785b58 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 11 Feb 2016 10:03:29 -1000 Subject: [PATCH 116/636] [skip ci] doc update --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 059302a3..31ebbdba 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The gem will automatically apply several headers that are related to security. If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. -All `nil` values will fallback to their default value. `SecureHeaders::OPT_OUT` will disable the header entirely. +All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. ```ruby SecureHeaders::Configuration.default do |config| @@ -36,8 +36,12 @@ SecureHeaders::Configuration.default do |config| config.x_download_options = "noopen" config.x_permitted_cross_domain_policies = "none" config.csp = { + # "meta" values. these will shaped the header, but the values are not included in the header. + report_only: true, # default: false + preserve_schemes: true # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. + + # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), - report_only: false, frame_src: %w('self' *.twimg.com itunes.apple.com), connect_src: %w(wws:), font_src: %w('self' data:), From 78ecd021fba15cfc26827de1acfefb06dfccdc0a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 11 Feb 2016 10:53:30 -1000 Subject: [PATCH 117/636] This is a hack. I'm not sure why, but the direct hash comparison with == would fail for arrays that were identical. Calling to_s reliably returns the right value. --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index d9dfc2f6..7b32ded9 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -181,7 +181,7 @@ def validate_config!(config) # because google.com is already in the config. def idempotent_additions?(config, additions) return false if config == OPT_OUT - config == combine_policies(config, additions) + config.to_s == combine_policies(config, additions).to_s end # Public: combine the values from two different configs. From 3de87f9eb8e95ab05c9e482942aa16e8f2c451b3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 11 Feb 2016 10:54:41 -1000 Subject: [PATCH 118/636] handle some edge cases around policy merging --- lib/secure_headers/headers/content_security_policy.rb | 4 ++-- .../secure_headers/headers/content_security_policy_spec.rb | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 7b32ded9..a512fbc3 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -213,11 +213,11 @@ def combine_policies(original, additions) # when each hash contains a value for a given key. original.merge(additions) do |directive, lhs, rhs| if source_list?(directive) - lhs | rhs + (lhs.to_a + rhs).uniq.compact else rhs end - end + end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives. end private diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 613869d5..aa7d4fa4 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -117,6 +117,9 @@ module SecureHeaders specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true } specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true } end describe "#value" do From 85eb110bc82555c67be0de1c90d1b73892239950 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 11 Feb 2016 12:29:18 -1000 Subject: [PATCH 119/636] version bump for (hopefully) final release --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 2186e89c..ed34b73f 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.0.0.pre2" + gem.version = "3.0.0.pre3" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 7d96d7ad7599fcd552a652605735650de79666d7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 11 Feb 2016 12:47:21 -1000 Subject: [PATCH 120/636] the new :preserve_schemes flag would throw an error if you actually tried to use it --- README.md | 4 +-- .../headers/content_security_policy.rb | 9 +++-- .../headers/content_security_policy_spec.rb | 35 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 31ebbdba..83b3bfe3 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ SecureHeaders::Configuration.default do |config| config.x_permitted_cross_domain_policies = "none" config.csp = { # "meta" values. these will shaped the header, but the values are not included in the header. - report_only: true, # default: false - preserve_schemes: true # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. + report_only: true, # default: false + preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 309a5167..da0dedf3 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -145,7 +145,12 @@ class ContentSecurityPolicy STAR, DATA_PROTOCOL, BLOB_PROTOCOL - ] + ].freeze + + META_CONFIGS = [ + :report_only, + :preserve_schemes + ].freeze class << self # Public: generate a header name, value array that is user-agent-aware. @@ -165,7 +170,7 @@ def validate_config!(config) return if config.nil? || config == OPT_OUT raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src] config.each do |key, value| - if key == :report_only + if META_CONFIGS.include?(key) raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil? else validate_directive!(key, value) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 93262000..7ab15c24 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -23,6 +23,35 @@ module SecureHeaders end describe "#validate_config!" do + it "accepts all keys" do + # (pulled from README) + config = { + # "meta" values. these will shaped the header, but the values are not included in the header. + report_only: true, # default: false + preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. + + # directive values: these values will directly translate into source directives + default_src: %w(https: 'self'), + frame_src: %w('self' *.twimg.com itunes.apple.com), + connect_src: %w(wws:), + font_src: %w('self' data:), + img_src: %w(mycdn.com data:), + media_src: %w(utoob.com), + object_src: %w('self'), + script_src: %w('self'), + style_src: %w('unsafe-inline'), + base_uri: %w('self'), + child_src: %w('self'), + form_action: %w('self' github.com), + frame_ancestors: %w('none'), + plugin_types: %w(application/x-shockwave-flash), + block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) + report_uri: %w(https://example.com/uri-directive) + } + + CSP.validate_config!(config) + end + it "requires a :default_src value" do expect do CSP.validate_config!(script_src: %('self')) @@ -35,6 +64,12 @@ module SecureHeaders end.to raise_error(ContentSecurityPolicyConfigError) end + it "requires :preserve_schemes to be a truthy value" do + expect do + CSP.validate_config!(default_opts.merge(preserve_schemes: "steve")) + end.to raise_error(ContentSecurityPolicyConfigError) + end + it "requires :block_all_mixed_content to be a boolean value" do expect do CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve")) From f4670eb8a775d434a15d39bdcaf359892fa4d7da Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 12 Feb 2016 13:48:25 -1000 Subject: [PATCH 121/636] Directive values may be nil, so secure_headers should not :boom: Supplying a directive with a value of `nil` could :boom: in two places: * During `idempotent_additions?` check * When generating the header --- lib/secure_headers/headers/content_security_policy.rb | 4 +++- .../secure_headers/headers/content_security_policy_spec.rb | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index da0dedf3..d264ec53 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -218,7 +218,7 @@ def combine_policies(original, additions) # when each hash contains a value for a given key. original.merge(additions) do |directive, lhs, rhs| if source_list?(directive) - (lhs.to_a + rhs).uniq.compact + (lhs.to_a + rhs.to_a).uniq.compact else rhs end @@ -343,6 +343,8 @@ def build_value # # Returns a string representing a directive. def build_directive(directive_name) + return if @config[directive_name].nil? + source_list = @config[directive_name].compact return if source_list.empty? diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 7ab15c24..bd91f664 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -155,6 +155,7 @@ module SecureHeaders specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true } specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true } specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true } end describe "#value" do @@ -201,6 +202,11 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org") end + it "does not add a directive if the value is nil" do + csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: nil) + expect(csp.value).to eq("default-src example.org") + end + it "deduplicates any source expressions" do csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org)) expect(csp.value).to eq("default-src example.org") From f6ccdc45572726ae7ba8c697236e284b42b6d671 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 12 Feb 2016 13:53:03 -1000 Subject: [PATCH 122/636] compact then unique, not vice versa --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index d264ec53..486442a8 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -218,7 +218,7 @@ def combine_policies(original, additions) # when each hash contains a value for a given key. original.merge(additions) do |directive, lhs, rhs| if source_list?(directive) - (lhs.to_a + rhs.to_a).uniq.compact + (lhs.to_a + rhs.to_a).compact.uniq else rhs end From 82e7b6a486c455eb95cc8739c664253108155f7b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 16 Feb 2016 09:49:58 -1000 Subject: [PATCH 123/636] version bump --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index ed34b73f..783938d1 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.0.0.pre3" + gem.version = "3.0.0.rc1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From dabcdcb6f76e556e44d269649f46525594016530 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 16 Feb 2016 10:54:34 -1000 Subject: [PATCH 124/636] Reintroduce CHANGELOG pulled from existing GitHub releases --- CHANGELOG.md | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..15b3b744 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,201 @@ +## 2.5.1 - 2016-02-16 18:11:11 UTC - Remove noisy deprecation warning + +See https://github.com/twitter/secureheaders/issues/203 and https://github.com/twitter/secureheaders/commit/cfad0e52285353b88e46fe384e7cd60bf2a01735 + +>> Upon upgrading to secure_headers 2.5.0, I get a flood of these deprecations when running my tests: +> [DEPRECATION] secure_header_options_for will not be supported in secure_headers + +/cc @bquorning + +## 2.5.0 - 2016-01-06 22:11:02 UTC - 2.x deprecation warning release + +This release contains deprecation warnings for those wishing to upgrade to the 3.x series. With this release, fixing all deprecation warnings will make your configuration compatible when you decide to upgrade to the soon-to-be-released 3.x series (currently in pre-release stage). + +No changes to functionality should be observed unless you were using procs as CSP config values. + +## 2.4.4 - 2015-12-03 23:29:42 UTC - Bug fix release + +If you use the `header_hash` method for setting your headers in middleware and you opted out of a header (via setting the value to `false`), you would run into an exception as described in https://github.com/twitter/secureheaders/pull/193 + +``` + NoMethodError: + undefined method `name' for nil:NilClass + # ./lib/secure_headers.rb:63:in `block in header_hash' + # ./lib/secure_headers.rb:54:in `each' + # ./lib/secure_headers.rb:54:in `inject' + # ./lib/secure_headers.rb:54:in `header_hash' +``` + + +## 2.4.3 - 2015-10-23 18:35:43 UTC - Performance improvement + +@igrep reported an anti-patter in use regarding [UserAgentParser](https://github.com/ua-parser/uap-ruby). This caused UserAgentParser to reload it's entire configuration set *twice** per request. Moving this to a cached constant prevents the constant reinstantiation and will improve performance. + +https://github.com/twitter/secureheaders/issues/187 + +## 2.4.2 - 2015-10-20 20:22:08 UTC - Bug fix release + +A nasty regression meant that many CSP configuration values were "reset" after the first request, one of these being the "enforce" flag. See https://github.com/twitter/secureheaders/pull/184 for the full list of fields that were affected. Thanks to @spdawson for reporting this https://github.com/twitter/secureheaders/issues/183 + +## 2.4.1 - 2015-10-14 22:57:41 UTC - More UA sniffing + +This release may change the output of headers based on per browser support. Unsupported directives will be omitted based on the user agent per request. See https://github.com/twitter/secureheaders/pull/179 + +p.s. this will likely be the last non-bugfix release for the 2.x line. 3.x will be a major change. Sneak preview: https://github.com/twitter/secureheaders/pull/181 + +## 2.4.0 - 2015-10-01 23:05:38 UTC - Some internal changes affecting behavior, but not functionality + +If you leveraged `secure_headers` automatic filling of empty directives, the header value will change but it should not affect how the browser applies the policy. The content of CSP reports may change if you do not update your policy. + +before +=== + +```ruby + config.csp = { + :default_src => "'self'" + } +``` +would produce `default-src 'self'; connect-src 'self'; frame-src 'self' ... etc.` + +after +=== + +```ruby + config.csp = { + :default_src => "'self'" + } +``` + +will produce `default-src 'self'` + +The reason for this is that a `default-src` violation was basically impossible to handle. Chrome sends an `effective-directive` which helps indicate what kind of violation occurred even if it fell back to `default-src`. This is part of the [CSP Level 2 spec](http://www.w3.org/TR/CSP2/#violation-report-effective-directive) so hopefully other browsers will implement this soon. + +Workaround +=== + +Just set the values yourself, but really a `default-src` of anything other than `'none'` implies the policy can be tightened dramatically. "ZOMG don't you work for github and doesn't github send a `default-src` of `*`???" Yes, this is true. I disagree with this but at the same time, github defines every single known directive that a browser supports so `default-src` will only apply if a new directive is introduced, and we'd rather fail open. For now. + +```ruby + config.csp = { + :default_src => "'self'", + :connect_src => "'self'", + :frame_src => "'self'" + ... etc. + } +``` + +Besides, relying on `default-src` is often not what you want and encourages an overly permissive policy. I've seen it. Seriously. `default-src 'unsafe-inline' 'unsafe-eval' https: http:;` That's terrible. + + +## 2.3.0 - 2015-09-30 19:43:09 UTC - Add header_hash feature for use in middleware. + +See https://github.com/twitter/secureheaders/issues/167 and https://github.com/twitter/secureheaders/pull/168 + +tl;dr is that there is a class method `SecureHeaders::header_hash` that will return a hash of header name => value pairs useful for merging with the rack header hash in middleware. + +## 2.2.4 - 2015-08-26 23:31:37 UTC - Print deprecation warning for 1.8.7 users + +As discussed in https://github.com/twitter/secureheaders/issues/154 + +## 2.2.3 - 2015-08-14 20:26:12 UTC - Adds ability to opt-out of automatically adding data: sources to img-src + +See https://github.com/twitter/secureheaders/pull/161 + +## 2.2.2 - 2015-07-02 21:18:38 UTC - Another option for config granularity. + +See https://github.com/twitter/secureheaders/pull/147 + +Allows you to override a controller method that returns a config in the context of the executing action. + +## 2.2.1 - 2015-06-24 21:01:57 UTC - When using nonces, do not include the nonce for safari / IE + +See https://github.com/twitter/secureheaders/pull/150 + +Safari will generate a warning that it doesn't support nonces. Safari will fall back to the `unsafe-inline`. Things will still work, but an ugly message is printed to the console. + +This opts out safari and IE users from the inline script protection. I haven't verified any IE behavior yet, so I'm just assuming it doesn't work. + +## 2.2.0 - 2015-06-18 22:01:23 UTC - Pass controller reference to callable config value expressions. + +https://github.com/twitter/secureheaders/pull/148 + +Facilitates better per-request config: + + `:enforce => lambda { |controller| controller.current_user.beta_testing? }` + +**NOTE** if you used `lambda` config values, this will raise an exception until you add the controller reference: + +bad: + +`lambda { true }` + +good: + +`lambda { |controller| true }` +`proc { true }` +`proc { |controller| true }` + +## v2.1.0 - 2015-05-07 18:34:56 UTC - Add hpkp support + +Includes https://github.com/twitter/secureheaders/pull/143 (which is really just https://github.com/twitter/secureheaders/pull/132) from @thirstscolr + + +## v2.0.2 - 2015-05-05 03:09:44 UTC - Add report_uri constant value + +Just a small change that adds a constant that was missing as reported in https://github.com/twitter/secureheaders/issues/141 + +## v2.0.1 - 2015-03-20 18:46:47 UTC - View Helpers Fixed + +Fixes an issue where view helpers (for nonces, hashes, etc) weren't available in views. + +## 2.0.0 - 2015-01-23 20:23:56 UTC - 2.0 + +This release contains support for more csp level 2 features such as the new directives, the script hash integration, and more. + +It also sets a new header by default: `X-Permitted-Cross-Domain-Policies` + +Support for hpkp is not included in this release as the implementations are still very unstable. + +:rocket: + +## v.2.0.0.pre2 - 2014-12-06 01:55:42 UTC - Adds X-Permitted-Cross-Domain-Policies support by default + +The only change between this and the first pre release is that the X-Permitted-Cross-Domain-Policies support is included. + +## v1.4.0 - 2014-12-06 01:54:48 UTC - Deprecate features in preparation for 2.0 + +This removes the forwarder and "experimental" feature. The forwarder wasn't well maintained and created a lot of headaches. Also, it was using an outdated certificate pack for compatibility. That's bad. The experimental feature wasn't really used and it complicated the codebase a lot. It's also a questionably useful API that is very confusing. + +## v2.0.0.pre - 2014-11-14 00:54:07 UTC - 2.0.0.pre - CSP level 2 support + +This release is intended to be ready for CSP level 2. Mainly, this means there is direct support for hash/nonce of inline content and includes many new directives (which do not inherit from default-src) + +## v1.3.4 - 2014-10-13 22:05:44 UTC - + +* Adds X-Download-Options support +* Adds support for X-XSS-Protection reporting +* Defers loading of rails engine for faster boot times + +## v1.3.3 - 2014-08-15 02:30:24 UTC - hsts preload confirmation value support + +@agl just made a new option for HSTS representing confirmation that a site wants to be included in a browser's preload list (https://hstspreload.appspot.com). + +This just adds a new 'preload' option to the HSTS settings to specify that option. + +## v1.3.2 - 2014-08-14 00:01:32 UTC - Add app tagging support + +Tagging Requests + +It's often valuable to send extra information in the report uri that is not available in the reports themselves. Namely, "was the policy enforced" and "where did the report come from" +```ruby +{ + :tag_report_uri => true, + :enforce => true, + :app_name => 'twitter', + :report_uri => 'csp_reports' +} +``` +Results in +``` +report-uri csp_reports?enforce=true&app_name=twitter +``` From c7cd38d256285d029fbd6ec5e2a0e94b7d54f93b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Feb 2016 20:49:17 -1000 Subject: [PATCH 125/636] prep 3.x release --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15b3b744..d193ab6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.0 + +secure_headers 3.0.0 is a near-complete, not-entirely-backward-compatible rewrite. Please see the [upgrade guide](https://github.com/twitter/secureheaders/blob/master/upgrading-to-3-0.md) for an in-depth explanation of the changes and the suggested upgrade path. + ## 2.5.1 - 2016-02-16 18:11:11 UTC - Remove noisy deprecation warning See https://github.com/twitter/secureheaders/issues/203 and https://github.com/twitter/secureheaders/commit/cfad0e52285353b88e46fe384e7cd60bf2a01735 diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 783938d1..1d9b1078 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.0.0.rc1" + gem.version = "3.0.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 41baf8bd88562306356e5f05d2ab2e1102d27371 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 18 Feb 2016 15:27:51 -1000 Subject: [PATCH 126/636] Minor documentation cleanup. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 83b3bfe3..e4844e48 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ By default, a noop configuration is provided. No headers will be set when this d ```ruby class MyController < ApplicationController def index - SecureHeaders::opt_out_of_all_protection(request) + SecureHeaders.opt_out_of_all_protection(request) end end ``` @@ -191,7 +191,7 @@ Code | Result #### Nonce -script/style-nonce can be used to whitelist inline content. To do this, call the SecureHeaders::content_security_policy_nonce then set the nonce attributes on the various tags. +script/style-nonce can be used to whitelist inline content. To do this, call the `SecureHeaders.content_security_policy_nonce` then set the nonce attributes on the various tags. Setting a nonce will also set 'unsafe-inline' for browsers that don't support nonces for backwards compatibility. 'unsafe-inline' is ignored if a nonce is present in a directive in compliant browsers. @@ -280,7 +280,7 @@ class Donkey < Sinatra::Application set :root, APP_ROOT get '/' do - SecureHeaders.override_x_frame_options(SecureHeaders::OPT_OUT) + SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT) haml :index end end From e69645852700cf136e8b3e2181a2e4c484ded7c5 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 25 Feb 2016 08:31:42 -1000 Subject: [PATCH 127/636] X-Permitted-Cross-Domain-Policies was listed twice See https://github.com/twitter/secureheaders/commit/32bb3f51e8d877baf1e3e4461663ae0b7b1347c0#commitcomment-16319052 :sparkles: @reedloden for the :eyes: --- lib/secure_headers/railtie.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index 04bab46d..ab852bbf 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -7,7 +7,7 @@ class Railtie < Rails::Railtie 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options', 'X-Content-Type-Options', 'Strict-Transport-Security', 'Content-Security-Policy', 'Content-Security-Policy-Report-Only', - 'X-Permitted-Cross-Domain-Policies', 'Public-Key-Pins', 'Public-Key-Pins-Report-Only'] + 'Public-Key-Pins', 'Public-Key-Pins-Report-Only'] initializer "secure_headers.middleware" do Rails.application.config.middleware.use SecureHeaders::Middleware From f251b33766567932f50a937017c3e3f4fe7ed857 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 25 Feb 2016 08:56:54 -1000 Subject: [PATCH 128/636] Add support for CSP upgrade-insecure-requests source expression In my testing, firefox and chrome (and opera) support this value. Safari, untested. Fixes #219 --- README.md | 1 + .../headers/content_security_policy.rb | 47 ++++++++++--------- .../headers/content_security_policy_spec.rb | 27 ++++++++--- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index e4844e48..8b8c1fdd 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ SecureHeaders::Configuration.default do |config| frame_ancestors: %w('none'), plugin_types: %w(application/x-shockwave-flash), block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) + upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://example.com/uri-directive) } config.hpkp = { diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 486442a8..f3b1d918 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -77,8 +77,10 @@ class ContentSecurityPolicy # All the directives that are not currently in a formal spec, but have # been implemented somewhere. BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content + UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests DIRECTIVES_DRAFT = [ - BLOCK_ALL_MIXED_CONTENT + BLOCK_ALL_MIXED_CONTENT, + UPGRADE_INSECURE_REQUESTS ].freeze SAFARI_DIRECTIVES = DIRECTIVES_1_0 @@ -90,7 +92,7 @@ class ContentSecurityPolicy ].freeze FIREFOX_DIRECTIVES = ( - DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES + DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES ).freeze CHROME_DIRECTIVES = ( @@ -114,25 +116,26 @@ class ContentSecurityPolicy OTHER = "Other".freeze DIRECTIVE_VALUE_TYPES = { - BASE_URI => :source_list, - BLOCK_ALL_MIXED_CONTENT => :boolean, - CHILD_SRC => :source_list, - CONNECT_SRC => :source_list, - DEFAULT_SRC => :source_list, - FONT_SRC => :source_list, - FORM_ACTION => :source_list, - FRAME_ANCESTORS => :source_list, - FRAME_SRC => :source_list, - IMG_SRC => :source_list, - MANIFEST_SRC => :source_list, - MEDIA_SRC => :source_list, - OBJECT_SRC => :source_list, - PLUGIN_TYPES => :source_list, - REFLECTED_XSS => :string, - REPORT_URI => :source_list, - SANDBOX => :string, - SCRIPT_SRC => :source_list, - STYLE_SRC => :source_list + BASE_URI => :source_list, + BLOCK_ALL_MIXED_CONTENT => :boolean, + CHILD_SRC => :source_list, + CONNECT_SRC => :source_list, + DEFAULT_SRC => :source_list, + FONT_SRC => :source_list, + FORM_ACTION => :source_list, + FRAME_ANCESTORS => :source_list, + FRAME_SRC => :source_list, + IMG_SRC => :source_list, + MANIFEST_SRC => :source_list, + MEDIA_SRC => :source_list, + OBJECT_SRC => :source_list, + PLUGIN_TYPES => :source_list, + REFLECTED_XSS => :string, + REPORT_URI => :source_list, + SANDBOX => :string, + SCRIPT_SRC => :source_list, + STYLE_SRC => :source_list, + UPGRADE_INSECURE_REQUESTS => :boolean }.freeze CONFIG_KEY = :csp @@ -196,7 +199,7 @@ def idempotent_additions?(config, additions) # # raises an error if the original config is OPT_OUT # - # 1. for non-source-list values (report_only, block_all_mixed_content), + # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests), # additions will overwrite the original value. # 2. if a value in additions does not exist in the original config, the # default-src value is included to match original behavior. diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index bd91f664..300d3b35 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -46,6 +46,7 @@ module SecureHeaders frame_ancestors: %w('none'), plugin_types: %w(application/x-shockwave-flash), block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) + upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://example.com/uri-directive) } @@ -76,6 +77,12 @@ module SecureHeaders end.to raise_error(ContentSecurityPolicyConfigError) end + it "requires :upgrade_insecure_requests to be a boolean value" do + expect do + CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve")) + end.to raise_error(ContentSecurityPolicyConfigError) + end + it "requires all source lists to be an array of strings" do expect do CSP.validate_config!(default_src: "steve") @@ -214,27 +221,33 @@ module SecureHeaders context "browser sniffing" do let (:complex_opts) do - ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = %w('self') } - .merge(block_all_mixed_content: true, reflected_xss: "block") - .merge(script_src: %w('self'), script_nonce: 123456) + ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) do |directive, hash| + hash[directive] = %w('self') + end.merge({ + block_all_mixed_content: true, + upgrade_insecure_requests: true, + reflected_xss: "block", + script_src: %w('self'), + script_nonce: 123456 + }) end it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'") + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") end it "does not filter any directives for Opera" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'") + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'") + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") end - it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do + it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'") end From 29541631f1609318c215533472701c318c136224 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 25 Feb 2016 09:01:11 -1000 Subject: [PATCH 129/636] X-Frame-Options was listed twice in the conflicting headers section --- lib/secure_headers/railtie.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index ab852bbf..daeb2c7e 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -3,7 +3,7 @@ module SecureHeaders class Railtie < Rails::Railtie isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0 - conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', 'X-Content-Type-Options', + conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options', 'X-Content-Type-Options', 'Strict-Transport-Security', 'Content-Security-Policy', 'Content-Security-Policy-Report-Only', From 180d6fa7e80eae6170190ac9068ad8f00b934539 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 26 Feb 2016 08:19:23 -1000 Subject: [PATCH 130/636] version bump to add upgrade-insecure-requests support --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d193ab6c..fa52ed4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.1 + +Adds `upgrade-insecure-requests` support for requests from Firefox and Chrome (and Opera). See [the spec](https://www.w3.org/TR/upgrade-insecure-requests/) for details. + ## 3.0.0 secure_headers 3.0.0 is a near-complete, not-entirely-backward-compatible rewrite. Please see the [upgrade guide](https://github.com/twitter/secureheaders/blob/master/upgrading-to-3-0.md) for an in-depth explanation of the changes and the suggested upgrade path. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 1d9b1078..1046a2a7 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.0.0" + gem.version = "3.0.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From c15cb32f93a49cba323ad02a6225477fd8908dee Mon Sep 17 00:00:00 2001 From: Matt Brictson Date: Sat, 27 Feb 2016 15:59:14 -0800 Subject: [PATCH 131/636] Remove ensure_security_headers for 3.x upgrade --- upgrading-to-3-0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/upgrading-to-3-0.md b/upgrading-to-3-0.md index 3d210a72..b65b418f 100644 --- a/upgrading-to-3-0.md +++ b/upgrading-to-3-0.md @@ -20,6 +20,7 @@ Migrating to 3.x from <= 2.x 1. Convert all instances of `self`/`none`/`eval`/`inline` to the corresponding values in the above table. 1. Convert all CSP space-delimited directives to an array of strings. 1. Convert all `enforce: true|false` to `report_only: true|false`. +1. Remove `ensure_security_headers` from controllers (3.x uses a middleware instead). Everything is terrible, why should I upgrade? == From 5c3f92863f126a3b138b7bc3ca70e3f0881f8786 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sun, 28 Feb 2016 18:28:18 -1000 Subject: [PATCH 132/636] Handle the case where the hash is frozen and we're adding a directive Previously, this would cause an exception: RuntimeError: can't modify frozen Hash ./lib/secure_headers/headers/content_security_policy.rb:216:in `block in combine_policies' ./lib/secure_headers/headers/content_security_policy.rb:214:in `each' ./lib/secure_headers/headers/content_security_policy.rb:214:in `combine_policies' ./spec/lib/secure_headers/headers/content_security_policy_spec.rb:127:in `block (3 levels) in ' --- lib/secure_headers/headers/content_security_policy.rb | 2 ++ .../headers/content_security_policy_spec.rb | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index f3b1d918..2cf6af2b 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -210,6 +210,8 @@ def combine_policies(original, additions) raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") end + original = original.dup # in case the hash is frozen + # in case we would be appending to an empty directive, fill it with the default-src value additions.keys.each do |directive| unless original[directive] || !source_list?(directive) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 300d3b35..3f5990fb 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -117,6 +117,17 @@ module SecureHeaders expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") end + it "combines directives where the original value is nil and the hash is frozen" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + report_only: false + }.freeze + end + combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: %w(https://report-uri.io/asdf)) + expect(combined_config[:report_uri]).to_not be_nil + end + it "overrides the report_only flag" do Configuration.default do |config| config.csp = { From 0c729118abb7fbb4b04b1d302a705f8adb839bb8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sun, 28 Feb 2016 18:33:11 -1000 Subject: [PATCH 133/636] ONLY dup if the hash is actuall frozen --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 2cf6af2b..a3d6e789 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -210,7 +210,7 @@ def combine_policies(original, additions) raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") end - original = original.dup # in case the hash is frozen + original = original.dup if original.frozen? # in case we would be appending to an empty directive, fill it with the default-src value additions.keys.each do |directive| From b6b1fb423a02da0bd9edbd3e397e64b0810f345c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sun, 28 Feb 2016 18:41:29 -1000 Subject: [PATCH 134/636] 3.0.2 version bump --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa52ed4c..2f879a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.2 + +Bug fix for handling CSP configs that supply a frozen hash. If a directive value is `nil`, then appending to a config with a frozen hash would cause an error since we're trying to modify a frozen hash. See https://github.com/twitter/secureheaders/pull/223. + ## 3.0.1 Adds `upgrade-insecure-requests` support for requests from Firefox and Chrome (and Opera). See [the spec](https://www.w3.org/TR/upgrade-insecure-requests/) for details. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 1046a2a7..b3a5a2ad 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.0.1" + gem.version = "3.0.2" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 1be7d00ba1880535bf9eef5ceb1e0a6116db6295 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sun, 28 Feb 2016 19:01:09 -1000 Subject: [PATCH 135/636] Handle policy merges when directives do not inherit from default-src Previously, non default source values would be merged with the default-src value which makes no sense. This was especially confusing if your default-src was `*` or `'none'` --- .../headers/content_security_policy.rb | 12 +++++++++- .../headers/content_security_policy_spec.rb | 23 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index a3d6e789..0357e2d6 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -55,6 +55,16 @@ class ContentSecurityPolicy FRAME_ANCESTORS = :frame_ancestors PLUGIN_TYPES = :plugin_types + # These are directives that do not inherit the default-src value. This is + # useful when calling #combine_policies. + NON_DEFAULT_SOURCES = [ + BASE_URI, + FORM_ACTION, + FRAME_ANCESTORS, + PLUGIN_TYPES, + REPORT_URI + ] + DIRECTIVES_2_0 = [ DIRECTIVES_1_0, BASE_URI, @@ -214,7 +224,7 @@ def combine_policies(original, additions) # in case we would be appending to an empty directive, fill it with the default-src value additions.keys.each do |directive| - unless original[directive] || !source_list?(directive) + unless original[directive] || !source_list?(directive) || NON_DEFAULT_SOURCES.include?(directive) original[directive] = original[:default_src] end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 3f5990fb..6984479d 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -124,8 +124,27 @@ module SecureHeaders report_only: false }.freeze end - combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: %w(https://report-uri.io/asdf)) - expect(combined_config[:report_uri]).to_not be_nil + report_uri = "https://report-uri.io/asdf" + combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri]) + csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) + expect(csp.value).to include("report-uri #{report_uri}") + end + + it "does not combine the default-src value for directives that don't fall back to default sources" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + report_only: false + }.freeze + end + non_default_source_additions = CSP::NON_DEFAULT_SOURCES.each_with_object({}) do |directive, hash| + hash[directive] = "http://example.org" + end + combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions) + + CSP::NON_DEFAULT_SOURCES.each do |directive| + expect(combined_config[directive]).to eq("http://example.org") + end end it "overrides the report_only flag" do From 248dcf200e3d096a162fc50106cbe9313c2c0844 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sun, 28 Feb 2016 19:09:13 -1000 Subject: [PATCH 136/636] wrap the configs in an array, ensure config is valid --- .../secure_headers/headers/content_security_policy_spec.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 6984479d..d1dee9f8 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -138,13 +138,15 @@ module SecureHeaders }.freeze end non_default_source_additions = CSP::NON_DEFAULT_SOURCES.each_with_object({}) do |directive, hash| - hash[directive] = "http://example.org" + hash[directive] = %w("http://example.org) end combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions) CSP::NON_DEFAULT_SOURCES.each do |directive| - expect(combined_config[directive]).to eq("http://example.org") + expect(combined_config[directive]).to eq(%w("http://example.org)) end + + ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value end it "overrides the report_only flag" do From 3622c1b7b682d0f0baeb955e00ea511eb58f2614 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 29 Feb 2016 06:43:15 -1000 Subject: [PATCH 137/636] 3.0.3 version bump --- CHANGELOG.md | 19 +++++++++++++++++++ secure_headers.gemspec | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f879a0b..f234ed2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## 3.0.3 + +Bug fix for handling policy merges where appending a non-default source value (report-uri, plugin-types, frame-ancestors, base-uri, and form-action) would be combined with the default-src value. Appending a directive that doesn't exist in the current policy combines the new value with `default-src` to mimic the actual behavior of the addition. However, this does not make sense for non-default-src values (a.k.a. "fetch directives") and can lead to unexpected behavior like a `report-uri` value of `*`. Previously, this config: + +``` +{ + default_src => %w(*) +} +``` + +When appending: + +``` +{ + report_uri => %w(https://report-uri.io/asdf) +} + +Would result in `default-src *; report-uri *` which doesn't make any sense at all. + ## 3.0.2 Bug fix for handling CSP configs that supply a frozen hash. If a directive value is `nil`, then appending to a config with a frozen hash would cause an error since we're trying to modify a frozen hash. See https://github.com/twitter/secureheaders/pull/223. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index b3a5a2ad..ab29dc8e 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.0.2" + gem.version = "3.0.3" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 5331c03a5856bb6bce5d303b017ef1f251c806c9 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 00:10:50 -1000 Subject: [PATCH 138/636] move constants and class methods to their own file --- .../headers/content_security_policy.rb | 290 +---------------- .../headers/policy_management.rb | 292 ++++++++++++++++++ .../headers/content_security_policy_spec.rb | 175 ----------- .../headers/policy_management_spec.rb | 190 ++++++++++++ 4 files changed, 484 insertions(+), 463 deletions(-) create mode 100644 lib/secure_headers/headers/policy_management.rb create mode 100644 spec/lib/secure_headers/headers/policy_management_spec.rb diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 0357e2d6..1a439ad0 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -1,296 +1,10 @@ -require 'uri' -require 'base64' require 'securerandom' -require 'json' +require_relative 'policy_management' module SecureHeaders class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicy - MODERN_BROWSERS = %w(Chrome Opera Firefox) - DEFAULT_VALUE = "default-src https:".freeze - DEFAULT_CONFIG = { default_src: %w(https:) }.freeze - HEADER_NAME = "Content-Security-Policy".freeze - REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze - HEADER_NAMES = [HEADER_NAME, REPORT_ONLY] - DATA_PROTOCOL = "data:".freeze - BLOB_PROTOCOL = "blob:".freeze - SELF = "'self'".freeze - NONE = "'none'".freeze - STAR = "*".freeze - UNSAFE_INLINE = "'unsafe-inline'".freeze - UNSAFE_EVAL = "'unsafe-eval'".freeze - - # leftover deprecated values that will be in common use upon upgrading. - DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze - - DEFAULT_SRC = :default_src - CONNECT_SRC = :connect_src - FONT_SRC = :font_src - FRAME_SRC = :frame_src - IMG_SRC = :img_src - MEDIA_SRC = :media_src - OBJECT_SRC = :object_src - SANDBOX = :sandbox - SCRIPT_SRC = :script_src - STYLE_SRC = :style_src - REPORT_URI = :report_uri - - DIRECTIVES_1_0 = [ - DEFAULT_SRC, - CONNECT_SRC, - FONT_SRC, - FRAME_SRC, - IMG_SRC, - MEDIA_SRC, - OBJECT_SRC, - SANDBOX, - SCRIPT_SRC, - STYLE_SRC, - REPORT_URI - ].freeze - - BASE_URI = :base_uri - CHILD_SRC = :child_src - FORM_ACTION = :form_action - FRAME_ANCESTORS = :frame_ancestors - PLUGIN_TYPES = :plugin_types - - # These are directives that do not inherit the default-src value. This is - # useful when calling #combine_policies. - NON_DEFAULT_SOURCES = [ - BASE_URI, - FORM_ACTION, - FRAME_ANCESTORS, - PLUGIN_TYPES, - REPORT_URI - ] - - DIRECTIVES_2_0 = [ - DIRECTIVES_1_0, - BASE_URI, - CHILD_SRC, - FORM_ACTION, - FRAME_ANCESTORS, - PLUGIN_TYPES - ].flatten.freeze - - # All the directives currently under consideration for CSP level 3. - # https://w3c.github.io/webappsec/specs/CSP2/ - MANIFEST_SRC = :manifest_src - REFLECTED_XSS = :reflected_xss - DIRECTIVES_3_0 = [ - DIRECTIVES_2_0, - MANIFEST_SRC, - REFLECTED_XSS - ].flatten.freeze - - # All the directives that are not currently in a formal spec, but have - # been implemented somewhere. - BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content - UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests - DIRECTIVES_DRAFT = [ - BLOCK_ALL_MIXED_CONTENT, - UPGRADE_INSECURE_REQUESTS - ].freeze - - SAFARI_DIRECTIVES = DIRECTIVES_1_0 - - FIREFOX_UNSUPPORTED_DIRECTIVES = [ - BLOCK_ALL_MIXED_CONTENT, - CHILD_SRC, - PLUGIN_TYPES - ].freeze - - FIREFOX_DIRECTIVES = ( - DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES - ).freeze - - CHROME_DIRECTIVES = ( - DIRECTIVES_2_0 + DIRECTIVES_DRAFT - ).freeze - - ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort - - # Think of default-src and report-uri as the beginning and end respectively, - # everything else is in between. - BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] - - VARIATIONS = { - "Chrome" => CHROME_DIRECTIVES, - "Opera" => CHROME_DIRECTIVES, - "Firefox" => FIREFOX_DIRECTIVES, - "Safari" => SAFARI_DIRECTIVES, - "Other" => CHROME_DIRECTIVES - }.freeze - - OTHER = "Other".freeze - - DIRECTIVE_VALUE_TYPES = { - BASE_URI => :source_list, - BLOCK_ALL_MIXED_CONTENT => :boolean, - CHILD_SRC => :source_list, - CONNECT_SRC => :source_list, - DEFAULT_SRC => :source_list, - FONT_SRC => :source_list, - FORM_ACTION => :source_list, - FRAME_ANCESTORS => :source_list, - FRAME_SRC => :source_list, - IMG_SRC => :source_list, - MANIFEST_SRC => :source_list, - MEDIA_SRC => :source_list, - OBJECT_SRC => :source_list, - PLUGIN_TYPES => :source_list, - REFLECTED_XSS => :string, - REPORT_URI => :source_list, - SANDBOX => :string, - SCRIPT_SRC => :source_list, - STYLE_SRC => :source_list, - UPGRADE_INSECURE_REQUESTS => :boolean - }.freeze - - CONFIG_KEY = :csp - STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) - HTTP_SCHEME_REGEX = %r{\Ahttps?://} - - WILDCARD_SOURCES = [ - UNSAFE_EVAL, - UNSAFE_INLINE, - STAR, - DATA_PROTOCOL, - BLOB_PROTOCOL - ].freeze - - META_CONFIGS = [ - :report_only, - :preserve_schemes - ].freeze - - class << self - # Public: generate a header name, value array that is user-agent-aware. - # - # Returns a default policy if no configuration is provided, or a - # header name and value based on the config. - def make_header(config, user_agent) - header = new(config, user_agent) - [header.name, header.value] - end - - # Public: Validates each source expression. - # - # Does not validate the invididual values of the source expression (e.g. - # script_src => h*t*t*p: will not raise an exception) - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src] - config.each do |key, value| - if META_CONFIGS.include?(key) - raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil? - else - validate_directive!(key, value) - end - end - end - - # Public: determine if merging +additions+ will cause a change to the - # actual value of the config. - # - # e.g. config = { script_src: %w(example.org google.com)} and - # additions = { script_src: %w(google.com)} then idempotent_additions? would return - # because google.com is already in the config. - def idempotent_additions?(config, additions) - return false if config == OPT_OUT - config.to_s == combine_policies(config, additions).to_s - end - - # Public: combine the values from two different configs. - # - # original - the main config - # additions - values to be merged in - # - # raises an error if the original config is OPT_OUT - # - # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests), - # additions will overwrite the original value. - # 2. if a value in additions does not exist in the original config, the - # default-src value is included to match original behavior. - # 3. if a value in additions does exist in the original config, the two - # values are joined. - def combine_policies(original, additions) - if original == OPT_OUT - raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") - end - - original = original.dup if original.frozen? - - # in case we would be appending to an empty directive, fill it with the default-src value - additions.keys.each do |directive| - unless original[directive] || !source_list?(directive) || NON_DEFAULT_SOURCES.include?(directive) - original[directive] = original[:default_src] - end - end - - # merge the two hashes. combine (instead of overwrite) the array values - # when each hash contains a value for a given key. - original.merge(additions) do |directive, lhs, rhs| - if source_list?(directive) - (lhs.to_a + rhs.to_a).compact.uniq - else - rhs - end - end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives. - end - - private - - def source_list?(directive) - DIRECTIVE_VALUE_TYPES[directive] == :source_list - end - - # Private: Validates that the configuration has a valid type, or that it is a valid - # source expression. - def validate_directive!(key, value) - case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key] - when :boolean - unless boolean?(value) - raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") - end - when :string - unless value.is_a?(String) - raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value") - end - else - validate_source_expression!(key, value) - end - end - - # Private: validates that a source expression: - # 1. has a valid name - # 2. is an array of strings - # 3. does not contain any depreated, now invalid values (inline, eval, self, none) - # - # Does not validate the invididual values of the source expression (e.g. - # script_src => h*t*t*p: will not raise an exception) - def validate_source_expression!(key, value) - unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key) - raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}") - end - - unless value.is_a?(Array) && value.compact.all? { |v| v.is_a?(String) } - raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings") - end - - value.each do |source_expression| - if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression) - raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.") - end - end - end - - def boolean?(value) - value.is_a?(TrueClass) || value.is_a?(FalseClass) - end - end - + include PolicyManagement def initialize(config = nil, user_agent = OTHER) config = Configuration.deep_copy(DEFAULT_CONFIG) unless config diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb new file mode 100644 index 00000000..9cbf788f --- /dev/null +++ b/lib/secure_headers/headers/policy_management.rb @@ -0,0 +1,292 @@ +module SecureHeaders + module PolicyManagement + def self.included(base) + base.extend(ClassMethods) + end + + MODERN_BROWSERS = %w(Chrome Opera Firefox) + DEFAULT_VALUE = "default-src https:".freeze + DEFAULT_CONFIG = { default_src: %w(https:) }.freeze + HEADER_NAME = "Content-Security-Policy".freeze + REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze + HEADER_NAMES = [HEADER_NAME, REPORT_ONLY] + DATA_PROTOCOL = "data:".freeze + BLOB_PROTOCOL = "blob:".freeze + SELF = "'self'".freeze + NONE = "'none'".freeze + STAR = "*".freeze + UNSAFE_INLINE = "'unsafe-inline'".freeze + UNSAFE_EVAL = "'unsafe-eval'".freeze + + # leftover deprecated values that will be in common use upon upgrading. + DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze + + DEFAULT_SRC = :default_src + CONNECT_SRC = :connect_src + FONT_SRC = :font_src + FRAME_SRC = :frame_src + IMG_SRC = :img_src + MEDIA_SRC = :media_src + OBJECT_SRC = :object_src + SANDBOX = :sandbox + SCRIPT_SRC = :script_src + STYLE_SRC = :style_src + REPORT_URI = :report_uri + + DIRECTIVES_1_0 = [ + DEFAULT_SRC, + CONNECT_SRC, + FONT_SRC, + FRAME_SRC, + IMG_SRC, + MEDIA_SRC, + OBJECT_SRC, + SANDBOX, + SCRIPT_SRC, + STYLE_SRC, + REPORT_URI + ].freeze + + BASE_URI = :base_uri + CHILD_SRC = :child_src + FORM_ACTION = :form_action + FRAME_ANCESTORS = :frame_ancestors + PLUGIN_TYPES = :plugin_types + + # These are directives that do not inherit the default-src value. This is + # useful when calling #combine_policies. + NON_DEFAULT_SOURCES = [ + BASE_URI, + FORM_ACTION, + FRAME_ANCESTORS, + PLUGIN_TYPES, + REPORT_URI + ] + + DIRECTIVES_2_0 = [ + DIRECTIVES_1_0, + BASE_URI, + CHILD_SRC, + FORM_ACTION, + FRAME_ANCESTORS, + PLUGIN_TYPES + ].flatten.freeze + + # All the directives currently under consideration for CSP level 3. + # https://w3c.github.io/webappsec/specs/CSP2/ + MANIFEST_SRC = :manifest_src + REFLECTED_XSS = :reflected_xss + DIRECTIVES_3_0 = [ + DIRECTIVES_2_0, + MANIFEST_SRC, + REFLECTED_XSS + ].flatten.freeze + + # All the directives that are not currently in a formal spec, but have + # been implemented somewhere. + BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content + UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests + DIRECTIVES_DRAFT = [ + BLOCK_ALL_MIXED_CONTENT, + UPGRADE_INSECURE_REQUESTS + ].freeze + + SAFARI_DIRECTIVES = DIRECTIVES_1_0 + + FIREFOX_UNSUPPORTED_DIRECTIVES = [ + BLOCK_ALL_MIXED_CONTENT, + CHILD_SRC, + PLUGIN_TYPES + ].freeze + + FIREFOX_DIRECTIVES = ( + DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES + ).freeze + + CHROME_DIRECTIVES = ( + DIRECTIVES_2_0 + DIRECTIVES_DRAFT + ).freeze + + ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort + + # Think of default-src and report-uri as the beginning and end respectively, + # everything else is in between. + BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] + + VARIATIONS = { + "Chrome" => CHROME_DIRECTIVES, + "Opera" => CHROME_DIRECTIVES, + "Firefox" => FIREFOX_DIRECTIVES, + "Safari" => SAFARI_DIRECTIVES, + "Other" => CHROME_DIRECTIVES + }.freeze + + OTHER = "Other".freeze + + DIRECTIVE_VALUE_TYPES = { + BASE_URI => :source_list, + BLOCK_ALL_MIXED_CONTENT => :boolean, + CHILD_SRC => :source_list, + CONNECT_SRC => :source_list, + DEFAULT_SRC => :source_list, + FONT_SRC => :source_list, + FORM_ACTION => :source_list, + FRAME_ANCESTORS => :source_list, + FRAME_SRC => :source_list, + IMG_SRC => :source_list, + MANIFEST_SRC => :source_list, + MEDIA_SRC => :source_list, + OBJECT_SRC => :source_list, + PLUGIN_TYPES => :source_list, + REFLECTED_XSS => :string, + REPORT_URI => :source_list, + SANDBOX => :string, + SCRIPT_SRC => :source_list, + STYLE_SRC => :source_list, + UPGRADE_INSECURE_REQUESTS => :boolean + }.freeze + + CONFIG_KEY = :csp + STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) + HTTP_SCHEME_REGEX = %r{\Ahttps?://} + + WILDCARD_SOURCES = [ + UNSAFE_EVAL, + UNSAFE_INLINE, + STAR, + DATA_PROTOCOL, + BLOB_PROTOCOL + ].freeze + + META_CONFIGS = [ + :report_only, + :preserve_schemes + ].freeze + + module ClassMethods + # Public: generate a header name, value array that is user-agent-aware. + # + # Returns a default policy if no configuration is provided, or a + # header name and value based on the config. + def make_header(config, user_agent) + header = new(config, user_agent) + [header.name, header.value] + end + + # Public: Validates each source expression. + # + # Does not validate the invididual values of the source expression (e.g. + # script_src => h*t*t*p: will not raise an exception) + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src] + config.each do |key, value| + if META_CONFIGS.include?(key) + raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil? + else + validate_directive!(key, value) + end + end + end + + # Public: determine if merging +additions+ will cause a change to the + # actual value of the config. + # + # e.g. config = { script_src: %w(example.org google.com)} and + # additions = { script_src: %w(google.com)} then idempotent_additions? would return + # because google.com is already in the config. + def idempotent_additions?(config, additions) + return false if config == OPT_OUT + config.to_s == combine_policies(config, additions).to_s + end + + # Public: combine the values from two different configs. + # + # original - the main config + # additions - values to be merged in + # + # raises an error if the original config is OPT_OUT + # + # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests), + # additions will overwrite the original value. + # 2. if a value in additions does not exist in the original config, the + # default-src value is included to match original behavior. + # 3. if a value in additions does exist in the original config, the two + # values are joined. + def combine_policies(original, additions) + if original == OPT_OUT + raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") + end + + original = original.dup if original.frozen? + + # in case we would be appending to an empty directive, fill it with the default-src value + additions.keys.each do |directive| + unless original[directive] || !source_list?(directive) || NON_DEFAULT_SOURCES.include?(directive) + original[directive] = original[:default_src] + end + end + + # merge the two hashes. combine (instead of overwrite) the array values + # when each hash contains a value for a given key. + original.merge(additions) do |directive, lhs, rhs| + if source_list?(directive) + (lhs.to_a + rhs.to_a).compact.uniq + else + rhs + end + end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives. + end + + private + + def source_list?(directive) + DIRECTIVE_VALUE_TYPES[directive] == :source_list + end + + # Private: Validates that the configuration has a valid type, or that it is a valid + # source expression. + def validate_directive!(key, value) + case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key] + when :boolean + unless boolean?(value) + raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") + end + when :string + unless value.is_a?(String) + raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value") + end + else + validate_source_expression!(key, value) + end + end + + # Private: validates that a source expression: + # 1. has a valid name + # 2. is an array of strings + # 3. does not contain any depreated, now invalid values (inline, eval, self, none) + # + # Does not validate the invididual values of the source expression (e.g. + # script_src => h*t*t*p: will not raise an exception) + def validate_source_expression!(key, value) + unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key) + raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}") + end + + unless value.is_a?(Array) && value.compact.all? { |v| v.is_a?(String) } + raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings") + end + + value.each do |source_expression| + if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression) + raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.") + end + end + end + + def boolean?(value) + value.is_a?(TrueClass) || value.is_a?(FalseClass) + end + end + end +end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index d1dee9f8..04e60fd9 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -22,181 +22,6 @@ module SecureHeaders end end - describe "#validate_config!" do - it "accepts all keys" do - # (pulled from README) - config = { - # "meta" values. these will shaped the header, but the values are not included in the header. - report_only: true, # default: false - preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. - - # directive values: these values will directly translate into source directives - default_src: %w(https: 'self'), - frame_src: %w('self' *.twimg.com itunes.apple.com), - connect_src: %w(wws:), - font_src: %w('self' data:), - img_src: %w(mycdn.com data:), - media_src: %w(utoob.com), - object_src: %w('self'), - script_src: %w('self'), - style_src: %w('unsafe-inline'), - base_uri: %w('self'), - child_src: %w('self'), - form_action: %w('self' github.com), - frame_ancestors: %w('none'), - plugin_types: %w(application/x-shockwave-flash), - block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) - upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ - report_uri: %w(https://example.com/uri-directive) - } - - CSP.validate_config!(config) - end - - it "requires a :default_src value" do - expect do - CSP.validate_config!(script_src: %('self')) - end.to raise_error(ContentSecurityPolicyConfigError) - end - - it "requires :report_only to be a truthy value" do - expect do - CSP.validate_config!(default_opts.merge(report_only: "steve")) - end.to raise_error(ContentSecurityPolicyConfigError) - end - - it "requires :preserve_schemes to be a truthy value" do - expect do - CSP.validate_config!(default_opts.merge(preserve_schemes: "steve")) - end.to raise_error(ContentSecurityPolicyConfigError) - end - - it "requires :block_all_mixed_content to be a boolean value" do - expect do - CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve")) - end.to raise_error(ContentSecurityPolicyConfigError) - end - - it "requires :upgrade_insecure_requests to be a boolean value" do - expect do - CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve")) - end.to raise_error(ContentSecurityPolicyConfigError) - end - - it "requires all source lists to be an array of strings" do - expect do - CSP.validate_config!(default_src: "steve") - end.to raise_error(ContentSecurityPolicyConfigError) - end - - it "allows nil values" do - expect do - CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil]) - end.to_not raise_error - end - - it "rejects unknown directives / config" do - expect do - CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve") - end.to raise_error(ContentSecurityPolicyConfigError) - end - - # this is mostly to ensure people don't use the antiquated shorthands common in other configs - it "performs light validation on source lists" do - expect do - CSP.validate_config!(default_src: %w(self none inline eval)) - end.to raise_error(ContentSecurityPolicyConfigError) - end - end - - describe "#combine_policies" do - it "combines the default-src value with the override if the directive was unconfigured" do - combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com)) - csp = ContentSecurityPolicy.new(combined_config) - expect(csp.name).to eq(CSP::HEADER_NAME) - expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") - end - - it "combines directives where the original value is nil and the hash is frozen" do - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - report_only: false - }.freeze - end - report_uri = "https://report-uri.io/asdf" - combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri]) - csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) - expect(csp.value).to include("report-uri #{report_uri}") - end - - it "does not combine the default-src value for directives that don't fall back to default sources" do - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - report_only: false - }.freeze - end - non_default_source_additions = CSP::NON_DEFAULT_SOURCES.each_with_object({}) do |directive, hash| - hash[directive] = %w("http://example.org) - end - combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions) - - CSP::NON_DEFAULT_SOURCES.each do |directive| - expect(combined_config[directive]).to eq(%w("http://example.org)) - end - - ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value - end - - it "overrides the report_only flag" do - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - report_only: false - } - end - combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true) - csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) - expect(csp.name).to eq(CSP::REPORT_ONLY) - end - - it "overrides the :block_all_mixed_content flag" do - Configuration.default do |config| - config.csp = { - default_src: %w(https:), - block_all_mixed_content: false - } - end - combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true) - csp = ContentSecurityPolicy.new(combined_config) - expect(csp.value).to eq("default-src https:; block-all-mixed-content") - end - - it "raises an error if appending to a OPT_OUT policy" do - Configuration.default do |config| - config.csp = OPT_OUT - end - expect do - CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com)) - end.to raise_error(ContentSecurityPolicyConfigError) - end - end - - describe "#idempotent_additions?" do - specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false } - - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true } - end - describe "#value" do it "discards 'none' values if any other source expressions are present" do csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none'))) diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb new file mode 100644 index 00000000..1a92b02a --- /dev/null +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -0,0 +1,190 @@ +require 'spec_helper' + +module SecureHeaders + describe PolicyManagement do + let (:default_opts) do + { + default_src: %w(https:), + img_src: %w(https: data:), + script_src: %w('unsafe-inline' 'unsafe-eval' https: data:), + style_src: %w('unsafe-inline' https: about:), + report_uri: %w(/csp_report) + } + end + + describe "#validate_config!" do + it "accepts all keys" do + # (pulled from README) + config = { + # "meta" values. these will shaped the header, but the values are not included in the header. + report_only: true, # default: false + preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. + + # directive values: these values will directly translate into source directives + default_src: %w(https: 'self'), + frame_src: %w('self' *.twimg.com itunes.apple.com), + connect_src: %w(wws:), + font_src: %w('self' data:), + img_src: %w(mycdn.com data:), + media_src: %w(utoob.com), + object_src: %w('self'), + script_src: %w('self'), + style_src: %w('unsafe-inline'), + base_uri: %w('self'), + child_src: %w('self'), + form_action: %w('self' github.com), + frame_ancestors: %w('none'), + plugin_types: %w(application/x-shockwave-flash), + block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) + upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ + report_uri: %w(https://example.com/uri-directive) + } + + CSP.validate_config!(config) + end + + it "requires a :default_src value" do + expect do + CSP.validate_config!(script_src: %('self')) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "requires :report_only to be a truthy value" do + expect do + CSP.validate_config!(default_opts.merge(report_only: "steve")) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "requires :preserve_schemes to be a truthy value" do + expect do + CSP.validate_config!(default_opts.merge(preserve_schemes: "steve")) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "requires :block_all_mixed_content to be a boolean value" do + expect do + CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve")) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "requires :upgrade_insecure_requests to be a boolean value" do + expect do + CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve")) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "requires all source lists to be an array of strings" do + expect do + CSP.validate_config!(default_src: "steve") + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "allows nil values" do + expect do + CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil]) + end.to_not raise_error + end + + it "rejects unknown directives / config" do + expect do + CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve") + end.to raise_error(ContentSecurityPolicyConfigError) + end + + # this is mostly to ensure people don't use the antiquated shorthands common in other configs + it "performs light validation on source lists" do + expect do + CSP.validate_config!(default_src: %w(self none inline eval)) + end.to raise_error(ContentSecurityPolicyConfigError) + end + end + + describe "#combine_policies" do + it "combines the default-src value with the override if the directive was unconfigured" do + combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com)) + csp = ContentSecurityPolicy.new(combined_config) + expect(csp.name).to eq(CSP::HEADER_NAME) + expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") + end + + it "combines directives where the original value is nil and the hash is frozen" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + report_only: false + }.freeze + end + report_uri = "https://report-uri.io/asdf" + combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri]) + csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) + expect(csp.value).to include("report-uri #{report_uri}") + end + + it "does not combine the default-src value for directives that don't fall back to default sources" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + report_only: false + }.freeze + end + non_default_source_additions = CSP::NON_DEFAULT_SOURCES.each_with_object({}) do |directive, hash| + hash[directive] = %w("http://example.org) + end + combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions) + + CSP::NON_DEFAULT_SOURCES.each do |directive| + expect(combined_config[directive]).to eq(%w("http://example.org)) + end + + ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value + end + + it "overrides the report_only flag" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + report_only: false + } + end + combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true) + csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) + expect(csp.name).to eq(CSP::REPORT_ONLY) + end + + it "overrides the :block_all_mixed_content flag" do + Configuration.default do |config| + config.csp = { + default_src: %w(https:), + block_all_mixed_content: false + } + end + combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true) + csp = ContentSecurityPolicy.new(combined_config) + expect(csp.value).to eq("default-src https:; block-all-mixed-content") + end + + it "raises an error if appending to a OPT_OUT policy" do + Configuration.default do |config| + config.csp = OPT_OUT + end + expect do + CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com)) + end.to raise_error(ContentSecurityPolicyConfigError) + end + end + + describe "#idempotent_additions?" do + specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false } + + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true } + specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true } + end + end +end From 9d8ce3fab638ae0c0ae9d2aebcd36e7665aefa4a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 00:11:27 -1000 Subject: [PATCH 139/636] rename non-default-sources as non-fetch-sources to match spec text --- lib/secure_headers/headers/policy_management.rb | 4 ++-- spec/lib/secure_headers/headers/policy_management_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 9cbf788f..87970267 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -55,7 +55,7 @@ def self.included(base) # These are directives that do not inherit the default-src value. This is # useful when calling #combine_policies. - NON_DEFAULT_SOURCES = [ + NON_FETCH_SOURCES = [ BASE_URI, FORM_ACTION, FRAME_ANCESTORS, @@ -222,7 +222,7 @@ def combine_policies(original, additions) # in case we would be appending to an empty directive, fill it with the default-src value additions.keys.each do |directive| - unless original[directive] || !source_list?(directive) || NON_DEFAULT_SOURCES.include?(directive) + unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive) original[directive] = original[:default_src] end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 1a92b02a..35e38167 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -127,12 +127,12 @@ module SecureHeaders report_only: false }.freeze end - non_default_source_additions = CSP::NON_DEFAULT_SOURCES.each_with_object({}) do |directive, hash| + non_default_source_additions = CSP::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions) - CSP::NON_DEFAULT_SOURCES.each do |directive| + CSP::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) end From 4a90c7d3ac159e60521c939938af16b41bffe25d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 00:20:47 -1000 Subject: [PATCH 140/636] extract more complex code into smaller methods, rename key/value to directive/source_expression --- .../headers/policy_management.rb | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 87970267..d875c03d 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -219,16 +219,15 @@ def combine_policies(original, additions) end original = original.dup if original.frozen? + populate_fetch_source_with_default!(original, additions) + merge_policy_additions(original, additions) + end - # in case we would be appending to an empty directive, fill it with the default-src value - additions.keys.each do |directive| - unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive) - original[directive] = original[:default_src] - end - end + private - # merge the two hashes. combine (instead of overwrite) the array values - # when each hash contains a value for a given key. + # merge the two hashes. combine (instead of overwrite) the array values + # when each hash contains a value for a given key. + def merge_policy_additions(original, additions) original.merge(additions) do |directive, lhs, rhs| if source_list?(directive) (lhs.to_a + rhs.to_a).compact.uniq @@ -238,7 +237,16 @@ def combine_policies(original, additions) end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives. end - private + # For each directive in additions that does not exist in the original config, + # copy the default-src value to the original config. This modifies the original hash. + def populate_fetch_source_with_default!(original, additions) + # in case we would be appending to an empty directive, fill it with the default-src value + additions.keys.each do |directive| + unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive) + original[directive] = original[:default_src] + end + end + end def source_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :source_list @@ -246,18 +254,18 @@ def source_list?(directive) # Private: Validates that the configuration has a valid type, or that it is a valid # source expression. - def validate_directive!(key, value) - case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key] + def validate_directive!(directive, source_expression) + case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive] when :boolean - unless boolean?(value) - raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") + unless boolean?(source_expression) + raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value") end when :string - unless value.is_a?(String) - raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value") + unless source_expression.is_a?(String) + raise ContentSecurityPolicyConfigError.new("#{directive} Must be a string. Found #{config.class}: #{config} value") end else - validate_source_expression!(key, value) + validate_source_expression!(directive, source_expression) end end @@ -268,24 +276,34 @@ def validate_directive!(key, value) # # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) - def validate_source_expression!(key, value) - unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key) - raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}") + def validate_source_expression!(directive, source_expression) + ensure_valid_directive!(directive) + ensure_array_of_strings!(directive, source_expression) + ensure_valid_sources!(directive, source_expression) + end + + def ensure_valid_directive!(directive) + unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(directive) + raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") end + end - unless value.is_a?(Array) && value.compact.all? { |v| v.is_a?(String) } - raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings") + def ensure_array_of_strings!(directive, source_expression) + unless source_expression.is_a?(Array) && source_expression.compact.all? { |v| v.is_a?(String) } + raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings") end + end - value.each do |source_expression| + def ensure_valid_sources!(directive, source_expression) + source_expression.each do |source_expression| if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression) - raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.") + raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{source_expression}). This value must be single quoted.") end end end - def boolean?(value) - value.is_a?(TrueClass) || value.is_a?(FalseClass) + def boolean?(source_expression) + source_expression.is_a?(TrueClass) || source_expression.is_a?(FalseClass) end end end From fadf3d2fc9d4d2f16c0ab0dc157de06e50d13fef Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 00:31:26 -1000 Subject: [PATCH 141/636] break up complex build_directive method --- .../headers/content_security_policy.rb | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 1a439ad0..aab6deeb 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -64,36 +64,45 @@ def build_value end # Private: builds a string that represents one directive in a minified form. - # If a directive contains *, all other values are omitted. - # If a directive contains 'none' but has other values, 'none' is ommitted. - # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression) # # directive_name - a symbol representing the various ALL_DIRECTIVES # # Returns a string representing a directive. - def build_directive(directive_name) - return if @config[directive_name].nil? + def build_directive(directive) + return if @config[directive].nil? - source_list = @config[directive_name].compact + source_list = @config[directive].compact return if source_list.empty? - value = if source_list.include?(STAR) - # Discard trailing entries (excluding unsafe-*) since * accomplishes the same. - source_list.select { |value| WILDCARD_SOURCES.include?(value) } - else - populate_nonces(directive_name, source_list) + normalized_source_list = minify_source_list(directive, source_list) + [symbol_to_hyphen_case(directive), normalized_source_list].join(" ") + end - # Discard any 'none' values if more directives are supplied since none may override values. - source_list.reject! { |value| value == NONE } if source_list.length > 1 + # If a directive contains *, all other values are omitted. + # If a directive contains 'none' but has other values, 'none' is ommitted. + # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression) + def minify_source_list(directive, source_list) + if source_list.include?(STAR) + keep_wildcard_sources(source_list) + else + populate_nonces!(directive, source_list) + reject_all_values_if_none!(source_list) - # remove schemes and dedup source expressions - unless directive_name == REPORT_URI || @preserve_schemes - source_list = strip_source_schemes(source_list) + unless directive == REPORT_URI || @preserve_schemes + strip_source_schemes!(source_list) end dedup_source_list(source_list).join(" ") end + end + + # Discard trailing entries (excluding unsafe-*) since * accomplishes the same. + def keep_wildcard_sources(source_list) + source_list.select { |value| WILDCARD_SOURCES.include?(value) } + end - [symbol_to_hyphen_case(directive_name), value].join(" ") + # Discard any 'none' values if more directives are supplied since none may override values. + def reject_all_values_if_none!(source_list) + source_list.reject! { |value| value == NONE } if source_list.length > 1 end # Removes duplicates and sources that already match an existing wild card. @@ -115,7 +124,7 @@ def dedup_source_list(sources) # Private: append a nonce to the script/style directories if script_nonce # or style_nonce are provided. - def populate_nonces(directive, source_list) + def populate_nonces!(directive, source_list) case directive when SCRIPT_SRC append_nonce(source_list, @script_nonce) @@ -148,8 +157,8 @@ def directives end # Private: Remove scheme from source expressions. - def strip_source_schemes(source_list) - source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } + def strip_source_schemes!(source_list) + source_list.map! { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } end # Private: determine which directives are supported for the given user agent. From 904bc4d43bc0dcaa5a93734e07324fd7df431a9e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 00:50:53 -1000 Subject: [PATCH 142/636] test more rubies, but allow failures for *head --- .travis.yml | 19 ++++++++++++++----- travis.sh | 10 ---------- 2 files changed, 14 insertions(+), 15 deletions(-) delete mode 100755 travis.sh diff --git a/.travis.yml b/.travis.yml index eb27c43f..6af697da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,20 @@ language: ruby rvm: - - "2.2" - - "2.1" - - "2.0.0" - - "1.9.3" - - "jruby-19mode" + - ruby-head + - 2.2 + - 2.1 + - 2.0.0 + - 1.9.3 + - jruby-19mode + - jruby-head + +matrix: + allow_failures: + - rvm: + - jruby-head + - ruby-head + fast_finish: true before_install: gem update bundler diff --git a/travis.sh b/travis.sh deleted file mode 100755 index 0a73b5ba..00000000 --- a/travis.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /bin/sh - -bundle install >> /dev/null && -bundle exec rspec --format progress spec && -cd fixtures/rails_3_2_12 && -bundle install >> /dev/null && -bundle exec rspec --format progress spec && -cd ../../fixtures/rails_3_2_12_no_init && -bundle install >> /dev/null && -bundle exec rspec spec \ No newline at end of file From 0659c7cf9abce6300b8d329780fdc82209821da5 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:00:57 -1000 Subject: [PATCH 143/636] remove fast_finish --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6af697da..96c8cee9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,8 @@ rvm: matrix: allow_failures: - - rvm: - - jruby-head - - ruby-head - fast_finish: true + - rvm: jruby-head + - rvm: ruby-head before_install: gem update bundler From debc6187866f7d9a392154f7cf34a16069f17ca5 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:19:19 -1000 Subject: [PATCH 144/636] don't require secure_random --- lib/secure_headers/headers/content_security_policy.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index aab6deeb..b009d7cb 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -1,4 +1,3 @@ -require 'securerandom' require_relative 'policy_management' module SecureHeaders From 9096456114cb250e6273c8204a804705bbc6b5b8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:19:26 -1000 Subject: [PATCH 145/636] remove misleading comment --- lib/secure_headers/headers/x_content_type_options.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index 76796db2..65e583e8 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -1,6 +1,6 @@ module SecureHeaders class XContentTypeOptionsConfigError < StandardError; end - # IE only + class XContentTypeOptions HEADER_NAME = "X-Content-Type-Options" DEFAULT_VALUE = "nosniff" From fefa9d25daa50f4567b80a7d58198991e45de82a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:20:24 -1000 Subject: [PATCH 146/636] remove padrino helper (it referenced undefined content anyways) --- lib/secure_headers/padrino.rb | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 lib/secure_headers/padrino.rb diff --git a/lib/secure_headers/padrino.rb b/lib/secure_headers/padrino.rb deleted file mode 100644 index 5a8904db..00000000 --- a/lib/secure_headers/padrino.rb +++ /dev/null @@ -1,13 +0,0 @@ -module SecureHeaders - module Padrino - class << self - ## - # Main class that register this extension. - # - def registered(app) - app.helpers SecureHeaders::InstanceMethods - end - alias_method :included, :registered - end - end -end From c48fc7ba0c0c264f76bfc24118c31230b78474c7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:22:07 -1000 Subject: [PATCH 147/636] cleanup .gitignore --- .gitignore | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.gitignore b/.gitignore index 8d3f8f5d..0b32062a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,17 +6,8 @@ .yardoc *.log Gemfile.lock -InstalledFiles _yardoc coverage -doc/ -lib/bundler/man pkg rdoc spec/reports -test/tmp -test/version_tmp -*tmp -*.sqlite3 -fixtures/rails_3_2_12_no_init/log -fixtures/rails_3_2_12/log From 44303844f67dccc84b8c79baaf2eacdcbda66b51 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:23:15 -1000 Subject: [PATCH 148/636] cleanup gemfile, move guard gems to guard group --- .travis.yml | 1 + Gemfile | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 96c8cee9..91c09075 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ matrix: - rvm: ruby-head before_install: gem update bundler +bundler_args: --without guard -j 3 sudo: false cache: bundler diff --git a/Gemfile b/Gemfile index f850d127..344a2d46 100644 --- a/Gemfile +++ b/Gemfile @@ -6,9 +6,12 @@ group :test do gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0 gem "pry-nav" gem "rack" + gem "rspec" + gem "coveralls", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] +end + +group :guard do gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] - gem "rspec", ">= 3.1" gem "growl" gem "rb-fsevent" - gem "coveralls", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] end From 1a16486f65c82897f25a2ec2fc25bf3f1f7c0782 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:26:59 -1000 Subject: [PATCH 149/636] [skip ci] remove padrino setup from README --- README.md | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/README.md b/README.md index 8b8c1fdd..05acc108 100644 --- a/README.md +++ b/README.md @@ -287,43 +287,6 @@ class Donkey < Sinatra::Application end ``` -### Using with Padrino - -You can use SecureHeaders for Padrino applications as well: - -In your `Gemfile`: - -```ruby - gem "secure_headers", require: 'secure_headers' -``` - -then in your `app.rb` file you can: - -```ruby -Padrino.use(SecureHeaders::Middleware) -require 'secure_headers/padrino' - -module Web - class App < Padrino::Application - register SecureHeaders::Padrino - - get '/' do - render 'index' - end - end -end -``` - -and in `config/boot.rb`: - -```ruby -def before_load - SecureHeaders::Configuration.default do |config| - ... - end -end -``` - ## Similar libraries * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers) From 038b06101b9f2f5b626f870a56abd8c917fb7226 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:30:25 -1000 Subject: [PATCH 150/636] add includeSubdomains; preload to docs and spec --- README.md | 2 +- .../secure_headers/headers/strict_transport_security_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05acc108..75edba84 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` ```ruby SecureHeaders::Configuration.default do |config| - config.hsts = "max-age=#{20.years.to_i}" + config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" config.x_xss_protection = "1; mode=block" diff --git a/spec/lib/secure_headers/headers/strict_transport_security_spec.rb b/spec/lib/secure_headers/headers/strict_transport_security_spec.rb index 9499b685..346c7080 100644 --- a/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +++ b/spec/lib/secure_headers/headers/strict_transport_security_spec.rb @@ -4,7 +4,7 @@ module SecureHeaders describe StrictTransportSecurity do describe "#value" do specify { expect(StrictTransportSecurity.make_header).to eq([StrictTransportSecurity::HEADER_NAME, StrictTransportSecurity::DEFAULT_VALUE]) } - specify { expect(StrictTransportSecurity.make_header("max-age=1234")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234"]) } + specify { expect(StrictTransportSecurity.make_header("max-age=1234; includeSubdomains; preload")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234; includeSubdomains; preload"]) } context "with an invalid configuration" do context "with a string argument" do From e5f40f8b7b0145c4c7a4395a15f31a8911a4f3b3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:32:18 -1000 Subject: [PATCH 151/636] add more references to report-uri-io --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 75edba84..b2064759 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,13 @@ SecureHeaders::Configuration.default do |config| plugin_types: %w(application/x-shockwave-flash), block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ - report_uri: %w(https://example.com/uri-directive) + report_uri: %w(https://report-uri.io/example-csp) } config.hpkp = { report_only: false, max_age: 60.days.to_i, include_subdomains: true, - report_uri: "https://example.com/uri-directive", + report_uri: "https://report-uri.io/example-hpkp", pins: [ {sha256: "abc"}, {sha256: "123"} @@ -255,7 +255,7 @@ config.hpkp = { {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} ], report_only: true, # defaults to false (report-only mode) - report_uri: '//example.com/uri-directive', + report_uri: 'https://report-uri.io/example-hpkp', app_name: 'example', tag_report_uri: true } From 213be5e1009ed8650fa782613274cb3b39c1259f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:35:47 -1000 Subject: [PATCH 152/636] add note about fetch directives and policy additions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b2064759..f9b5edd9 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ When manipulating content security policy, there are a few things to consider. T #### Append to the policy with a directive other than `default_src` -The value of `default_src` is joined with the addition. Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate: +The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate: ```ruby ::SecureHeaders::Configuration.default do |config| From d5f4094f9a51604e77fadf98c670da76e9f4782e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:45:14 -1000 Subject: [PATCH 153/636] add coveralls support back --- spec/spec_helper.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4520396b..5e02e0ba 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,9 +3,12 @@ require 'rack' require 'pry-nav' +require 'coveralls' +Coveralls.wear! + require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers') -ENV["RAILS_ENV"] = "test" + USER_AGENTS = { firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1', From d35b310df4d2d24ed45e5a986adec5ed0b63b45b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 Mar 2016 01:47:31 -1000 Subject: [PATCH 154/636] re-add coveralls support --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 344a2d46..f1c33ce8 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ group :test do gem "pry-nav" gem "rack" gem "rspec" - gem "coveralls", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] + gem "coveralls" end group :guard do From 3ea1953bc8f8ad52cea8806bcfd0a364f341247c Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 3 Mar 2016 22:14:27 -0500 Subject: [PATCH 155/636] add link to dropwizard-web-security --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f9b5edd9..3dc97230 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,7 @@ end * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) * Go - [secureheader](https://github.com/kr/secureheader) * Elixir [secure_headers](https://github.com/anotherhale/secure_headers) +* Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security) ## License From 4f6181e1e8ce42d539aec60c10051c1085d1e7af Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Fri, 11 Mar 2016 10:33:19 -0500 Subject: [PATCH 156/636] add link to ember-cli-content-security-policy --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3dc97230..af73cd45 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,7 @@ end * Go - [secureheader](https://github.com/kr/secureheader) * Elixir [secure_headers](https://github.com/anotherhale/secure_headers) * Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security) +* Ember.js [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/) ## License From e6bfd7f6812a3837df275fda05614e6ed7332f12 Mon Sep 17 00:00:00 2001 From: Joey Geiger Date: Mon, 14 Mar 2016 11:10:23 -0600 Subject: [PATCH 157/636] Fix missing ``` in changelog Fix changelog formatting --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f234ed2b..bd4c324f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ When appending: { report_uri => %w(https://report-uri.io/asdf) } +``` Would result in `default-src *; report-uri *` which doesn't make any sense at all. From ce3c737fe345b2231198e845b081d8f46e4ac20b Mon Sep 17 00:00:00 2001 From: Julich Mera Date: Tue, 15 Mar 2016 16:43:44 -0400 Subject: [PATCH 158/636] We would like to give users the ability to flag all cookies as secure. https://www.owasp.org/index.php/SecureFlag --- lib/secure_headers/configuration.rb | 2 +- lib/secure_headers/middleware.rb | 26 +++++++++++++++++++ lib/secure_headers/railtie.rb | 2 +- spec/lib/secure_headers/configuration_spec.rb | 2 +- spec/lib/secure_headers/middleware_spec.rb | 24 ++++++++++++++--- 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index be2b0c24..d97aff82 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -90,7 +90,7 @@ def add_noop_configuration attr_accessor :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :csp, :x_download_options, :x_permitted_cross_domain_policies, - :hpkp + :hpkp, :secure_cookies attr_reader :cached_headers def initialize(&block) diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 02e8472e..06130097 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -1,5 +1,7 @@ module SecureHeaders class Middleware + SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze + def initialize(app) @app = app end @@ -8,8 +10,32 @@ def initialize(app) def call(env) req = Rack::Request.new(env) status, headers, response = @app.call(env) + + flag_cookies_as_secure!(headers) if config(req).secure_cookies headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] end + + private + + # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194 + def flag_cookies_as_secure!(headers) + if cookies = headers['Set-Cookie'] + # Support Rails 2.3 / Rack 1.1 arrays as headers + cookies = cookies.split("\n") unless cookies.is_a?(Array) + + headers['Set-Cookie'] = cookies.map do |cookie| + if cookie !~ SECURE_COOKIE_REGEXP + "#{cookie}; secure" + else + cookie + end + end.join("\n") + end + end + + def config(req) + req.env[SECURE_HEADERS_CONFIG] || Configuration.get(Configuration::DEFAULT_CONFIG) + end end end diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index daeb2c7e..efa80be2 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -10,7 +10,7 @@ class Railtie < Rails::Railtie 'Public-Key-Pins', 'Public-Key-Pins-Report-Only'] initializer "secure_headers.middleware" do - Rails.application.config.middleware.use SecureHeaders::Middleware + Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware end initializer "secure_headers.action_controller" do diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 59d9d5a2..496acb6b 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -37,7 +37,7 @@ module SecureHeaders config = Configuration.get(:test_override) noop = Configuration.get(Configuration::NOOP_CONFIGURATION) [:hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, - :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp].each do |key| + :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp, :secure_cookies].each do |key| expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}." end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 418f88f4..81b4dbba 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -3,10 +3,10 @@ module SecureHeaders describe Middleware do let(:app) { ->(env) { [200, env, "app"] } } + let(:cookie_app) { -> (env) { [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } } - let :middleware do - Middleware.new(app) - end + let(:middleware) { Middleware.new(app) } + let(:cookie_middleware) { Middleware.new(cookie_app) } before(:each) do reset_config @@ -36,5 +36,23 @@ module SecureHeaders _, env = middleware.call request.env expect(env[CSP::HEADER_NAME]).to match("example.org") end + + context "cookies should be flagged" do + it "flags cookies as secure" do + Configuration.default { |config| config.secure_cookies = true } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP) + end + end + + context "cookies should not be flagged" do + it "does not flags cookies as secure" do + Configuration.default { |config| config.secure_cookies = false } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).not_to match(Middleware::SECURE_COOKIE_REGEXP) + end + end end end From 0cd223a97978424c57f91bac22fc460ab6203967 Mon Sep 17 00:00:00 2001 From: Julich Mera Date: Tue, 15 Mar 2016 17:00:40 -0400 Subject: [PATCH 159/636] Make CI happy, use longer lambda syntax --- spec/lib/secure_headers/middleware_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 81b4dbba..2bc9be95 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -2,8 +2,8 @@ module SecureHeaders describe Middleware do - let(:app) { ->(env) { [200, env, "app"] } } - let(:cookie_app) { -> (env) { [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } } + let(:app) { lambda { |env| [200, env, "app"] } } + let(:cookie_app) { lambda { |env| [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } } let(:middleware) { Middleware.new(app) } let(:cookie_middleware) { Middleware.new(cookie_app) } From 3301a0d02e1ad28955e414aeb9ab08be1e476ea9 Mon Sep 17 00:00:00 2001 From: Julich Mera Date: Tue, 15 Mar 2016 17:20:09 -0400 Subject: [PATCH 160/636] Make SecureHeaders.config_for public allowing us to access config from different contexts. --- lib/secure_headers.rb | 20 ++++++++++---------- lib/secure_headers/middleware.rb | 7 ++----- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 0f48d92f..0a1b5416 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -149,6 +149,16 @@ def content_security_policy_style_nonce(request) content_security_policy_nonce(request, CSP::STYLE_SRC) end + # Public: Retreives the config for a given header type: + # + # Checks to see if there is an override for this request, then + # Checks to see if a named override is used for this request, then + # Falls back to the global config + def config_for(request) + request.env[SECURE_HEADERS_CONFIG] || + Configuration.get(Configuration::DEFAULT_CONFIG) + end + private # Private: gets or creates a nonce for CSP. @@ -217,16 +227,6 @@ def use_cached_headers(default_headers, request) end end - # Private: Retreives the config for a given header type: - # - # Checks to see if there is an override for this request, then - # Checks to see if a named override is used for this request, then - # Falls back to the global config - def config_for(request) - request.env[SECURE_HEADERS_CONFIG] || - Configuration.get(Configuration::DEFAULT_CONFIG) - end - # Private: chooses the applicable CSP header for the provided user agent. # # headers - a hash of header_config_key => [header_name, header_value] diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 06130097..603359bc 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -11,7 +11,8 @@ def call(env) req = Rack::Request.new(env) status, headers, response = @app.call(env) - flag_cookies_as_secure!(headers) if config(req).secure_cookies + config = SecureHeaders.config_for(req) + flag_cookies_as_secure!(headers) if config.secure_cookies headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] end @@ -33,9 +34,5 @@ def flag_cookies_as_secure!(headers) end.join("\n") end end - - def config(req) - req.env[SECURE_HEADERS_CONFIG] || Configuration.get(Configuration::DEFAULT_CONFIG) - end end end From 393960bd6f4b42474e3dce843bb4c2e80dd79110 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 15 Mar 2016 12:40:57 -1000 Subject: [PATCH 161/636] Only perform idempotency check once per request, only bust CSP cache on changes --- lib/secure_headers.rb | 51 ++++++++++--------- lib/secure_headers/configuration.rb | 41 ++++++++++----- .../headers/policy_management.rb | 8 +++ 3 files changed, 63 insertions(+), 37 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 0f48d92f..e7be4432 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -48,14 +48,13 @@ class << self # script_src: %w(another-host.com) def override_content_security_policy_directives(request, additions) config = config_for(request) - unless CSP.idempotent_additions?(config.csp, additions) - config = config.dup - if config.csp == OPT_OUT - config.csp = {} - end - config.csp.merge!(additions) - override_secure_headers_request_config(request, config) + if config.current_csp == OPT_OUT + config.csp = config.dynamic_csp = {} end + + csp = Configuration.deep_copy(config.current_csp) + config.dynamic_csp = csp.merge!(additions) + override_secure_headers_request_config(request, config) end # Public: appends source values to the current configuration. If no value @@ -66,11 +65,9 @@ def override_content_security_policy_directives(request, additions) # script_src: %w(another-host.com) def append_content_security_policy_directives(request, additions) config = config_for(request) - unless CSP.idempotent_additions?(config.csp, additions) - config = config.dup - config.csp = CSP.combine_policies(config.csp, additions) - override_secure_headers_request_config(request, config) - end + csp = Configuration.deep_copy(config.current_csp) + config.dynamic_csp = CSP.combine_policies(csp, additions) + override_secure_headers_request_config(request, config) end # Public: override X-Frame-Options settings for this request. @@ -79,16 +76,20 @@ def append_content_security_policy_directives(request, additions) # # Returns the current config def override_x_frame_options(request, value) - default_config = config_for(request).dup - default_config.x_frame_options = value - override_secure_headers_request_config(request, default_config) + config = config_for(request) + config.x_frame_options = value + config.cached_headers[XFrameOptions::CONFIG_KEY] = make_header(XFrameOptions, config.x_frame_options) + override_secure_headers_request_config(request, config) end # Public: opts out of setting a given header by creating a temporary config # and setting the given headers config to OPT_OUT. def opt_out_of_header(request, header_key) - config = config_for(request).dup + config = config_for(request) config.send("#{header_key}=", OPT_OUT) + if header_key == CSP::CONFIG_KEY + config.dynamic_csp = OPT_OUT + end override_secure_headers_request_config(request, config) end @@ -109,6 +110,9 @@ def opt_out_of_all_protection(request) # in Rack middleware. def header_hash_for(request) config = config_for(request) + unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp) + config.rebuild_csp_header_cache!(request.user_agent) + end headers = if cached_headers = config.cached_headers use_cached_headers(cached_headers, request) @@ -223,8 +227,14 @@ def use_cached_headers(default_headers, request) # Checks to see if a named override is used for this request, then # Falls back to the global config def config_for(request) - request.env[SECURE_HEADERS_CONFIG] || + config = request.env[SECURE_HEADERS_CONFIG] || Configuration.get(Configuration::DEFAULT_CONFIG) + + if config.frozen? + config.dup + else + config + end end # Private: chooses the applicable CSP header for the provided user agent. @@ -233,12 +243,7 @@ def config_for(request) # # Returns a CSP [header, value] array def default_csp_header_for_ua(headers, request) - family = UserAgent.parse(request.user_agent).browser - if CSP::VARIATIONS.key?(family) - headers[family] - else - headers[CSP::OTHER] - end + headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))] end # Private: optionally build a header with a given configure diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index be2b0c24..f3054b6f 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -90,8 +90,7 @@ def add_noop_configuration attr_accessor :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :csp, :x_download_options, :x_permitted_cross_domain_policies, - :hpkp - attr_reader :cached_headers + :hpkp, :dynamic_csp, :cached_headers def initialize(&block) self.hpkp = OPT_OUT @@ -110,18 +109,21 @@ def dup copy.x_xss_protection = x_xss_protection copy.x_download_options = x_download_options copy.x_permitted_cross_domain_policies = x_permitted_cross_domain_policies - copy.csp = if csp.is_a?(Hash) - self.class.deep_copy(csp) - else - csp - end + copy.csp = deep_copy_hash(csp) + copy.dynamic_csp = deep_copy_hash(dynamic_csp) + copy.hpkp = deep_copy_hash(hpkp) + copy.cached_headers = deep_copy_hash(cached_headers) + copy + end - copy.hpkp = if hpkp.is_a?(Hash) - self.class.deep_copy(hpkp) - else - hpkp + # Public: generated cached headers for a specific user agent. + def rebuild_csp_header_cache!(user_agent) + self.cached_headers[CSP::CONFIG_KEY] = {} + unless current_csp == OPT_OUT + user_agent = UserAgent.parse(user_agent) + variation = CSP.ua_to_variation(user_agent) + self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent) end - copy end # Public: Retrieve a config based on the CONFIG_KEY for a class @@ -133,6 +135,10 @@ def fetch(key) config end + def current_csp + self.dynamic_csp || self.csp + end + # Public: validates all configurations values. # # Raises various configuration errors if any invalid config is detected. @@ -177,13 +183,20 @@ def cache_headers! def generate_csp_headers(headers) unless csp == OPT_OUT headers[CSP::CONFIG_KEY] = {} - + csp_config = fetch(:current_csp) CSP::VARIATIONS.each do |name, _| - csp_config = fetch(CSP::CONFIG_KEY) csp = CSP.make_header(csp_config, UserAgent.parse(name)) headers[CSP::CONFIG_KEY][name] = csp.freeze end end end + + def deep_copy_hash(hash) + if hash.is_a?(Hash) + self.class.deep_copy(hash) + else + hash + end + end end end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index d875c03d..ff43c65d 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -223,6 +223,14 @@ def combine_policies(original, additions) merge_policy_additions(original, additions) end + def ua_to_variation(user_agent) + if family = user_agent.browser && VARIATIONS.key?(family) + VARIATIONS[family] + else + OTHER + end + end + private # merge the two hashes. combine (instead of overwrite) the array values From 9ffd52f5833599255a7876259ff4744ab42f76e5 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 15 Mar 2016 12:49:44 -1000 Subject: [PATCH 162/636] cached_headers will always contain the desired values --- lib/secure_headers.rb | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index e7be4432..10ab4a56 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -114,13 +114,7 @@ def header_hash_for(request) config.rebuild_csp_header_cache!(request.user_agent) end - headers = if cached_headers = config.cached_headers - use_cached_headers(cached_headers, request) - else - build_headers(config, request) - end - - headers + use_cached_headers(config.cached_headers, request) end # Public: specify which named override will be used for this request. @@ -185,36 +179,17 @@ def header_classes_for(request) end end - # Private: do the heavy lifting of converting a configuration object - # to a hash of headers valid for this request. - # - # Returns a hash of header names / values. - def build_headers(config, request) - header_classes_for(request).each_with_object({}) do |klass, hash| - header_config = if config - config.fetch(klass::CONFIG_KEY) - end - - header_name, value = if klass == CSP - make_header(klass, header_config, request.user_agent) - else - make_header(klass, header_config) - end - hash[header_name] = value if value - end - end - # Private: takes a precomputed hash of headers and returns the Headers # customized for the request. # # Returns a hash of header names / values valid for a given request. - def use_cached_headers(default_headers, request) + def use_cached_headers(headers, request) header_classes_for(request).each_with_object({}) do |klass, hash| - if default_header = default_headers[klass::CONFIG_KEY] + if header = headers[klass::CONFIG_KEY] header_name, value = if klass == CSP - default_csp_header_for_ua(default_header, request) + csp_header_for_ua(header, request) else - default_header + header end hash[header_name] = value end @@ -242,7 +217,7 @@ def config_for(request) # headers - a hash of header_config_key => [header_name, header_value] # # Returns a CSP [header, value] array - def default_csp_header_for_ua(headers, request) + def csp_header_for_ua(headers, request) headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))] end From c723aa317176f76066dfbc0a656cf5c813d9c89e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 15 Mar 2016 13:48:52 -1000 Subject: [PATCH 163/636] merge! call can be replaced with merge --- lib/secure_headers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 10ab4a56..d70a462e 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -53,7 +53,7 @@ def override_content_security_policy_directives(request, additions) end csp = Configuration.deep_copy(config.current_csp) - config.dynamic_csp = csp.merge!(additions) + config.dynamic_csp = csp.merge(additions) override_secure_headers_request_config(request, config) end From 355599616dcbd13d94d2f6d80c1dac7e8afcd57d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 15 Mar 2016 14:30:56 -1000 Subject: [PATCH 164/636] handle dup'ing the secure_cookies config value --- lib/secure_headers/configuration.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 6b3f981a..554d2f47 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -103,6 +103,7 @@ def initialize(&block) # Returns a deep-dup'd copy of this configuration. def dup copy = self.class.new + copy.secure_cookies = secure_cookies copy.hsts = hsts copy.x_frame_options = x_frame_options copy.x_content_type_options = x_content_type_options From c965ad87a59987b9fa42b256bc139dde83f4e7da Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 16 Mar 2016 08:35:11 -1000 Subject: [PATCH 165/636] there is no reason to deep dup config values since we can assume the configs we are modifying are dups of the originals --- lib/secure_headers.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 9b4405d0..1afd9ef5 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -52,8 +52,7 @@ def override_content_security_policy_directives(request, additions) config.csp = config.dynamic_csp = {} end - csp = Configuration.deep_copy(config.current_csp) - config.dynamic_csp = csp.merge(additions) + config.dynamic_csp = config.current_csp.merge(additions) override_secure_headers_request_config(request, config) end @@ -65,8 +64,7 @@ def override_content_security_policy_directives(request, additions) # script_src: %w(another-host.com) def append_content_security_policy_directives(request, additions) config = config_for(request) - csp = Configuration.deep_copy(config.current_csp) - config.dynamic_csp = CSP.combine_policies(csp, additions) + config.dynamic_csp = CSP.combine_policies(config.current_csp, additions) override_secure_headers_request_config(request, config) end From 437f604265e46a2d606c0e321c589535376cdb95 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 16 Mar 2016 08:55:44 -1000 Subject: [PATCH 166/636] move cache management code for xframeoptions inside Configuration class. make cached_headers an attr_reader again --- lib/secure_headers.rb | 3 +-- lib/secure_headers/configuration.rb | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 1afd9ef5..e9f589fa 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -75,8 +75,7 @@ def append_content_security_policy_directives(request, additions) # Returns the current config def override_x_frame_options(request, value) config = config_for(request) - config.x_frame_options = value - config.cached_headers[XFrameOptions::CONFIG_KEY] = make_header(XFrameOptions, config.x_frame_options) + config.update_x_frame_options(value) override_secure_headers_request_config(request, config) end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 554d2f47..1010707f 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -90,7 +90,9 @@ def add_noop_configuration attr_accessor :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :csp, :x_download_options, :x_permitted_cross_domain_policies, - :hpkp, :dynamic_csp, :cached_headers, :secure_cookies + :hpkp, :dynamic_csp, :secure_cookies + + attr_reader :cached_headers def initialize(&block) self.hpkp = OPT_OUT @@ -113,10 +115,15 @@ def dup copy.csp = deep_copy_hash(csp) copy.dynamic_csp = deep_copy_hash(dynamic_csp) copy.hpkp = deep_copy_hash(hpkp) - copy.cached_headers = deep_copy_hash(cached_headers) + copy.send(:cached_headers=, deep_copy_hash(cached_headers)) copy end + def update_x_frame_options(value) + self.x_frame_options = value + self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(self.x_frame_options) + end + # Public: generated cached headers for a specific user agent. def rebuild_csp_header_cache!(user_agent) self.cached_headers[CSP::CONFIG_KEY] = {} @@ -156,6 +163,12 @@ def validate_config! PublicKeyPins.validate_config!(hpkp) end + private + + def cached_headers=(headers) + @cached_headers = headers + end + # Public: Precompute the header names and values for this configuraiton. # Ensures that headers generated at configure time, not on demand. # @@ -172,7 +185,7 @@ def cache_headers! generate_csp_headers(headers) headers.freeze - @cached_headers = headers + self.cached_headers = headers end # Private: adds CSP headers for each variation of CSP support. From 15258b1684382ab7526c068c5709f84dcf4905cc Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 16 Mar 2016 08:56:53 -1000 Subject: [PATCH 167/636] mark cached_headers setter method as protected to avoid a send call --- lib/secure_headers/configuration.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 1010707f..deb101bd 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -115,7 +115,7 @@ def dup copy.csp = deep_copy_hash(csp) copy.dynamic_csp = deep_copy_hash(dynamic_csp) copy.hpkp = deep_copy_hash(hpkp) - copy.send(:cached_headers=, deep_copy_hash(cached_headers)) + copy.cached_headers = deep_copy_hash(cached_headers) copy end @@ -163,12 +163,14 @@ def validate_config! PublicKeyPins.validate_config!(hpkp) end - private + protected def cached_headers=(headers) @cached_headers = headers end + private + # Public: Precompute the header names and values for this configuraiton. # Ensures that headers generated at configure time, not on demand. # From 89ba87e4a74b1f76c28f8c73e2a45ebae7d2a110 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 16 Mar 2016 09:08:08 -1000 Subject: [PATCH 168/636] cleanup API visibility a little --- lib/secure_headers/configuration.rb | 65 +++++++++++++---------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index deb101bd..b136923d 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -43,18 +43,6 @@ def get(name = DEFAULT_CONFIG) @configurations[name] end - # Public: perform a basic deep dup. The shallow copy provided by dup/clone - # can lead to modifying parent objects. - def deep_copy(config) - config.each_with_object({}) do |(key, value), hash| - hash[key] = if value.is_a?(Array) - value.dup - else - value - end - end - end - private # Private: add a valid configuration to the global set of named configs. @@ -86,6 +74,28 @@ def add_noop_configuration add_configuration(NOOP_CONFIGURATION, noop_config) end + + # Public: perform a basic deep dup. The shallow copy provided by dup/clone + # can lead to modifying parent objects. + def deep_copy(config) + config.each_with_object({}) do |(key, value), hash| + hash[key] = if value.is_a?(Array) + value.dup + else + value + end + end + end + + # Private: convenience method purely DRY things up. The value may not be a + # hash (e.g. OPT_OUT, nil) + def deep_copy_if_hash(value) + if value.is_a?(Hash) + deep_copy(value) + else + value + end + end end attr_accessor :hsts, :x_frame_options, :x_content_type_options, @@ -96,7 +106,7 @@ def add_noop_configuration def initialize(&block) self.hpkp = OPT_OUT - self.csp = self.class.deep_copy(CSP::DEFAULT_CONFIG) + self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG) instance_eval &block if block_given? end @@ -112,10 +122,10 @@ def dup copy.x_xss_protection = x_xss_protection copy.x_download_options = x_download_options copy.x_permitted_cross_domain_policies = x_permitted_cross_domain_policies - copy.csp = deep_copy_hash(csp) - copy.dynamic_csp = deep_copy_hash(dynamic_csp) - copy.hpkp = deep_copy_hash(hpkp) - copy.cached_headers = deep_copy_hash(cached_headers) + copy.csp = self.class.send(:deep_copy_if_hash, csp) + copy.dynamic_csp = self.class.send(:deep_copy_if_hash, dynamic_csp) + copy.hpkp = self.class.send(:deep_copy_if_hash, hpkp) + copy.cached_headers = self.class.send(:deep_copy_if_hash, cached_headers) copy end @@ -134,15 +144,6 @@ def rebuild_csp_header_cache!(user_agent) end end - # Public: Retrieve a config based on the CONFIG_KEY for a class - # - # Returns the value if available, and returns a dup of any hash values. - def fetch(key) - config = send(key) - config = self.class.deep_copy(config) if config.is_a?(Hash) - config - end - def current_csp self.dynamic_csp || self.csp end @@ -178,7 +179,7 @@ def cached_headers=(headers) def cache_headers! # generate defaults for the "easy" headers headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash| - config = fetch(klass::CONFIG_KEY) + config = send(klass::CONFIG_KEY) unless config == OPT_OUT hash[klass::CONFIG_KEY] = klass.make_header(config).freeze end @@ -199,20 +200,12 @@ def cache_headers! def generate_csp_headers(headers) unless csp == OPT_OUT headers[CSP::CONFIG_KEY] = {} - csp_config = fetch(:current_csp) + csp_config = self.current_csp CSP::VARIATIONS.each do |name, _| csp = CSP.make_header(csp_config, UserAgent.parse(name)) headers[CSP::CONFIG_KEY][name] = csp.freeze end end end - - def deep_copy_hash(hash) - if hash.is_a?(Hash) - self.class.deep_copy(hash) - else - hash - end - end end end From 4d2b8e7062ef8d12c7f0efbc46d9b252954f1c5b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 16 Mar 2016 09:19:23 -1000 Subject: [PATCH 169/636] ensure that headers other than xfo/csp can bypassed for a given request (and the cache is updated) --- lib/secure_headers.rb | 5 +---- lib/secure_headers/configuration.rb | 8 ++++++++ spec/lib/secure_headers_spec.rb | 2 ++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index e9f589fa..9cc94587 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -83,10 +83,7 @@ def override_x_frame_options(request, value) # and setting the given headers config to OPT_OUT. def opt_out_of_header(request, header_key) config = config_for(request) - config.send("#{header_key}=", OPT_OUT) - if header_key == CSP::CONFIG_KEY - config.dynamic_csp = OPT_OUT - end + config.opt_out(header_key) override_secure_headers_request_config(request, config) end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index b136923d..cdba356f 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -129,6 +129,14 @@ def dup copy end + def opt_out(header) + send("#{header}=", OPT_OUT) + if header == CSP::CONFIG_KEY + dynamic_csp = OPT_OUT + end + self.cached_headers.delete(header) + end + def update_x_frame_options(value) self.x_frame_options = value self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(self.x_frame_options) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index f00e89fe..99d04be5 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -35,9 +35,11 @@ module SecureHeaders it "allows you to opt out of individual headers" do Configuration.default SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY) + SecureHeaders.opt_out_of_header(@request, XContentTypeOptions::CONFIG_KEY) hash = SecureHeaders.header_hash_for(@request) expect(hash['Content-Security-Policy-Report-Only']).to be_nil expect(hash['Content-Security-Policy']).to be_nil + expect(hash['X-Content-Type-Options']).to be_nil end it "allows you to opt out entirely" do From 621da3b18cfeef3600d0bc735d4e00dd74df201c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 16 Mar 2016 11:02:58 -1000 Subject: [PATCH 170/636] update header cache directly rather than updating config values (except CSP) --- lib/secure_headers/configuration.rb | 44 ++++++++----------- spec/lib/secure_headers/configuration_spec.rb | 6 +-- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index cdba356f..ab226e40 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -98,11 +98,11 @@ def deep_copy_if_hash(value) end end - attr_accessor :hsts, :x_frame_options, :x_content_type_options, + attr_writer :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :csp, :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :dynamic_csp, :secure_cookies - attr_reader :cached_headers + attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies def initialize(&block) self.hpkp = OPT_OUT @@ -115,17 +115,10 @@ def initialize(&block) # Returns a deep-dup'd copy of this configuration. def dup copy = self.class.new - copy.secure_cookies = secure_cookies - copy.hsts = hsts - copy.x_frame_options = x_frame_options - copy.x_content_type_options = x_content_type_options - copy.x_xss_protection = x_xss_protection - copy.x_download_options = x_download_options - copy.x_permitted_cross_domain_policies = x_permitted_cross_domain_policies - copy.csp = self.class.send(:deep_copy_if_hash, csp) - copy.dynamic_csp = self.class.send(:deep_copy_if_hash, dynamic_csp) - copy.hpkp = self.class.send(:deep_copy_if_hash, hpkp) - copy.cached_headers = self.class.send(:deep_copy_if_hash, cached_headers) + copy.secure_cookies = @secure_cookies + copy.csp = self.class.send(:deep_copy_if_hash, @csp) + copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp) + copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) copy end @@ -138,8 +131,7 @@ def opt_out(header) end def update_x_frame_options(value) - self.x_frame_options = value - self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(self.x_frame_options) + self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value) end # Public: generated cached headers for a specific user agent. @@ -153,7 +145,7 @@ def rebuild_csp_header_cache!(user_agent) end def current_csp - self.dynamic_csp || self.csp + @dynamic_csp || @csp end # Public: validates all configurations values. @@ -162,14 +154,14 @@ def current_csp # # Returns nothing def validate_config! - StrictTransportSecurity.validate_config!(hsts) - ContentSecurityPolicy.validate_config!(csp) - XFrameOptions.validate_config!(x_frame_options) - XContentTypeOptions.validate_config!(x_content_type_options) - XXssProtection.validate_config!(x_xss_protection) - XDownloadOptions.validate_config!(x_download_options) - XPermittedCrossDomainPolicies.validate_config!(x_permitted_cross_domain_policies) - PublicKeyPins.validate_config!(hpkp) + StrictTransportSecurity.validate_config!(@hsts) + ContentSecurityPolicy.validate_config!(@csp) + XFrameOptions.validate_config!(@x_frame_options) + XContentTypeOptions.validate_config!(@x_content_type_options) + XXssProtection.validate_config!(@x_xss_protection) + XDownloadOptions.validate_config!(@x_download_options) + XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies) + PublicKeyPins.validate_config!(@hpkp) end protected @@ -187,7 +179,7 @@ def cached_headers=(headers) def cache_headers! # generate defaults for the "easy" headers headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash| - config = send(klass::CONFIG_KEY) + config = instance_variable_get("@#{klass::CONFIG_KEY}") unless config == OPT_OUT hash[klass::CONFIG_KEY] = klass.make_header(config).freeze end @@ -206,7 +198,7 @@ def cache_headers! # # Returns nothing def generate_csp_headers(headers) - unless csp == OPT_OUT + unless @csp == OPT_OUT headers[CSP::CONFIG_KEY] = {} csp_config = self.current_csp CSP::VARIATIONS.each do |name, _| diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 496acb6b..01556193 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -29,16 +29,14 @@ module SecureHeaders expect_default_values(header_hash) end - it "copies all config values except for the cached headers when dup" do + it "copies config values when duping" do Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do # do nothing, just copy it end config = Configuration.get(:test_override) noop = Configuration.get(Configuration::NOOP_CONFIGURATION) - [:hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, - :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp, :secure_cookies].each do |key| - + [:csp, :dynamic_csp].each do |key| expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}." end end From 15791edbeddd3df00eae5e78680399340c13f405 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 17 Mar 2016 08:25:17 -1000 Subject: [PATCH 171/636] to_s was hiding a potential bug --- lib/secure_headers/headers/policy_management.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index ff43c65d..135ce93c 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -197,7 +197,7 @@ def validate_config!(config) # because google.com is already in the config. def idempotent_additions?(config, additions) return false if config == OPT_OUT - config.to_s == combine_policies(config, additions).to_s + config == combine_policies(config, additions) end # Public: combine the values from two different configs. @@ -218,7 +218,7 @@ def combine_policies(original, additions) raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") end - original = original.dup if original.frozen? + original = original.dup populate_fetch_source_with_default!(original, additions) merge_policy_additions(original, additions) end From 79e1eb14dcaf0962c1fc44f3d470ceb6651448c9 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 17 Mar 2016 08:54:44 -1000 Subject: [PATCH 172/636] use deep_copy in CombinePolicies just in case --- lib/secure_headers/headers/policy_management.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 135ce93c..672c065d 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -218,7 +218,7 @@ def combine_policies(original, additions) raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") end - original = original.dup + original = Configuration.send(:deep_copy, original) populate_fetch_source_with_default!(original, additions) merge_policy_additions(original, additions) end From bda2b58195b13ea2fb48f6dc83745e0875cb5c62 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 21 Mar 2016 19:47:17 -0700 Subject: [PATCH 173/636] add test ensuring that appending/overriding config dups a global policy once, and modifies a local policy on subsequent modifications --- spec/lib/secure_headers_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 99d04be5..9e46aa0d 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -109,6 +109,22 @@ module SecureHeaders expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end + it "dups global configuration just once when overriding n times" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + end + + SecureHeaders.append_content_security_policy_directives(@request, script_src: %w(anothercdn.com)) + new_config = SecureHeaders.config_for(@request) + expect(new_config).to_not be(SecureHeaders::Configuration.get) + + SecureHeaders.override_content_security_policy_directives(@request, script_src: %w(yet.anothercdn.com)) + current_config = SecureHeaders.config_for(@request) + expect(current_config).to be(new_config) + end + it "overrides individual directives" do Configuration.default do |config| config.csp = { From 2aa51ea2e45e4264219fe483bd5191a23d820215 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 21 Mar 2016 19:55:19 -0700 Subject: [PATCH 174/636] use let construct instead of an ivar for the request object --- spec/lib/secure_headers_spec.rb | 68 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 9e46aa0d..b7015895 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -16,27 +16,29 @@ module SecureHeaders before(:each) do reset_config - @request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") + end + let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") } + it "raises a NotYetConfiguredError if default has not been set" do expect do - SecureHeaders.header_hash_for(@request) + SecureHeaders.header_hash_for(request) end.to raise_error(Configuration::NotYetConfiguredError) end it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do expect do - SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY) end.to raise_error(Configuration::NotYetConfiguredError) end describe "#header_hash_for" do it "allows you to opt out of individual headers" do Configuration.default - SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY) - SecureHeaders.opt_out_of_header(@request, XContentTypeOptions::CONFIG_KEY) - hash = SecureHeaders.header_hash_for(@request) + SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY) + hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy-Report-Only']).to be_nil expect(hash['Content-Security-Policy']).to be_nil expect(hash['X-Content-Type-Options']).to be_nil @@ -44,8 +46,8 @@ module SecureHeaders it "allows you to opt out entirely" do Configuration.default - SecureHeaders.opt_out_of_all_protection(@request) - hash = SecureHeaders.header_hash_for(@request) + SecureHeaders.opt_out_of_all_protection(request) + hash = SecureHeaders.header_hash_for(request) ALL_HEADER_CLASSES.each do |klass| expect(hash[klass::CONFIG_KEY]).to be_nil end @@ -53,8 +55,8 @@ module SecureHeaders it "allows you to override X-Frame-Options settings" do Configuration.default - SecureHeaders.override_x_frame_options(@request, XFrameOptions::DENY) - hash = SecureHeaders.header_hash_for(@request) + SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY) + hash = SecureHeaders.header_hash_for(request) expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY) end @@ -64,17 +66,17 @@ module SecureHeaders config.csp = OPT_OUT end - SecureHeaders.override_x_frame_options(@request, XFrameOptions::SAMEORIGIN) - SecureHeaders.override_content_security_policy_directives(@request, default_src: %w(https:), script_src: %w('self')) + SecureHeaders.override_x_frame_options(request, XFrameOptions::SAMEORIGIN) + SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self')) - hash = SecureHeaders.header_hash_for(@request) + hash = SecureHeaders.header_hash_for(request) expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'") expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN) end it "produces a hash of headers with default config" do Configuration.default - hash = SecureHeaders.header_hash_for(@request) + hash = SecureHeaders.header_hash_for(request) expect_default_values(hash) end @@ -104,8 +106,8 @@ module SecureHeaders } end - SecureHeaders.append_content_security_policy_directives(@request, script_src: %w(anothercdn.com)) - hash = SecureHeaders.header_hash_for(@request) + SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) + hash = SecureHeaders.header_hash_for(request) expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end @@ -116,12 +118,12 @@ module SecureHeaders } end - SecureHeaders.append_content_security_policy_directives(@request, script_src: %w(anothercdn.com)) - new_config = SecureHeaders.config_for(@request) + SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) + new_config = SecureHeaders.config_for(request) expect(new_config).to_not be(SecureHeaders::Configuration.get) - SecureHeaders.override_content_security_policy_directives(@request, script_src: %w(yet.anothercdn.com)) - current_config = SecureHeaders.config_for(@request) + SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com)) + current_config = SecureHeaders.config_for(request) expect(current_config).to be(new_config) end @@ -131,15 +133,15 @@ module SecureHeaders default_src: %w('self') } end - SecureHeaders.override_content_security_policy_directives(@request, default_src: %w('none')) - hash = SecureHeaders.header_hash_for(@request) + SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none')) + hash = SecureHeaders.header_hash_for(request) expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'") end it "overrides non-existant directives" do Configuration.default - SecureHeaders.override_content_security_policy_directives(@request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) - hash = SecureHeaders.header_hash_for(@request) + SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) + hash = SecureHeaders.header_hash_for(request) expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:") end @@ -152,9 +154,9 @@ module SecureHeaders } end - request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5])) - nonce = SecureHeaders.content_security_policy_script_nonce(request) - hash = SecureHeaders.header_hash_for(request) + safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5])) + nonce = SecureHeaders.content_security_policy_script_nonce(safari_request) + hash = SecureHeaders.header_hash_for(safari_request) expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'") end @@ -167,15 +169,15 @@ module SecureHeaders } end - request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome])) - nonce = SecureHeaders.content_security_policy_script_nonce(request) + chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome])) + nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request) # simulate the nonce being used multiple times in a request: - SecureHeaders.content_security_policy_script_nonce(request) - SecureHeaders.content_security_policy_script_nonce(request) - SecureHeaders.content_security_policy_script_nonce(request) + SecureHeaders.content_security_policy_script_nonce(chrome_request) + SecureHeaders.content_security_policy_script_nonce(chrome_request) + SecureHeaders.content_security_policy_script_nonce(chrome_request) - hash = SecureHeaders.header_hash_for(request) + hash = SecureHeaders.header_hash_for(chrome_request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") end end From 20ec917a1f1366db05fa2552d79fd0a4d2347876 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 21 Mar 2016 19:56:04 -0700 Subject: [PATCH 175/636] whitespace --- spec/lib/secure_headers_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index b7015895..5f44ea97 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -16,7 +16,6 @@ module SecureHeaders before(:each) do reset_config - end let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") } From e1b4b59f03897bc5480c025c033d5bcfae48023a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 21 Mar 2016 19:56:47 -0700 Subject: [PATCH 176/636] inline one-time use variable --- spec/lib/secure_headers_spec.rb | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 5f44ea97..9f77babc 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -2,18 +2,6 @@ module SecureHeaders describe SecureHeaders do - example_hpkp_config = { - max_age: 1_000_000, - include_subdomains: true, - report_uri: '//example.com/uri-directive', - pins: [ - { sha256: 'abc' }, - { sha256: '123' } - ] - } - - example_hpkp_config_value = %(max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains) - before(:each) do reset_config end @@ -90,7 +78,15 @@ module SecureHeaders it "does not set the HPKP header if request is over HTTP" do plaintext_request = Rack::Request.new({}) Configuration.default do |config| - config.hpkp = example_hpkp_config + config.hpkp = { + max_age: 1_000_000, + include_subdomains: true, + report_uri: '//example.com/uri-directive', + pins: [ + { sha256: 'abc' }, + { sha256: '123' } + ] + } end expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil From fb072472e99f3ed93da87fb097385f9e573d8aa9 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 21 Mar 2016 20:05:48 -0700 Subject: [PATCH 177/636] no need to namespace the Configuration class --- spec/lib/secure_headers_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 9f77babc..e89a98b3 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -115,7 +115,7 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) new_config = SecureHeaders.config_for(request) - expect(new_config).to_not be(SecureHeaders::Configuration.get) + expect(new_config).to_not be(Configuration.get) SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com)) current_config = SecureHeaders.config_for(request) From 00ce0c1a0f6217344b161d24dcd12067e5b6f0e3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 21 Mar 2016 20:12:42 -0700 Subject: [PATCH 178/636] add test statements showing env is void of a config --- spec/lib/secure_headers_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index e89a98b3..f13ac384 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -113,6 +113,9 @@ module SecureHeaders } end + # before an override occurs, the env is empty + expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil + SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) new_config = SecureHeaders.config_for(request) expect(new_config).to_not be(Configuration.get) From 7e16537512576440cfb521a457fb3656b0e71aaf Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 21 Mar 2016 20:24:39 -0700 Subject: [PATCH 179/636] add test clause ensuring that idempotent_additions? is only called once --- spec/lib/secure_headers_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index f13ac384..c37f24c7 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -106,13 +106,15 @@ module SecureHeaders expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end - it "dups global configuration just once when overriding n times" do + it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do Configuration.default do |config| config.csp = { default_src: %w('self') } end + expect(CSP).to receive(:idempotent_additions?).once + # before an override occurs, the env is empty expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil @@ -123,6 +125,8 @@ module SecureHeaders SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com)) current_config = SecureHeaders.config_for(request) expect(current_config).to be(new_config) + + SecureHeaders.header_hash_for(request) end it "overrides individual directives" do From 8a314e194b644988e8f5db39e1e73f7d8f68ab2c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 21 Mar 2016 22:17:13 -0700 Subject: [PATCH 180/636] add expectation that overriding the config stores the expected instance in env --- spec/lib/secure_headers/middleware_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 2bc9be95..c5d4b73b 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -33,6 +33,7 @@ module SecureHeaders end request = Rack::Request.new({}) SecureHeaders.use_secure_headers_override(request, "my_custom_config") + expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config")) _, env = middleware.call request.env expect(env[CSP::HEADER_NAME]).to match("example.org") end From cbe7014fdf85152df48fdba4988649a6dc893fc7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 22 Mar 2016 14:40:22 -1000 Subject: [PATCH 181/636] prevent the ability to muck with original CSP settings when using a dynamic policy /cc @ptoomey3 --- lib/secure_headers.rb | 2 +- lib/secure_headers/configuration.rb | 15 ++++++++++++--- spec/lib/secure_headers/configuration_spec.rb | 2 +- spec/lib/secure_headers_spec.rb | 18 ++++++++++++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 9cc94587..8a7e2cee 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -49,7 +49,7 @@ class << self def override_content_security_policy_directives(request, additions) config = config_for(request) if config.current_csp == OPT_OUT - config.csp = config.dynamic_csp = {} + config.dynamic_csp = {} end config.dynamic_csp = config.current_csp.merge(additions) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index ab226e40..f7a3ff16 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -3,6 +3,7 @@ class Configuration DEFAULT_CONFIG = :default NOOP_CONFIGURATION = "secure_headers_noop_config" class NotYetConfiguredError < StandardError; end + class IllegalPolicyModificationError < StandardError; end class << self # Public: Set the global default configuration. # @@ -23,12 +24,12 @@ def default(&block) # if no value is supplied. # # Returns: the newly created config - def override(name, base = DEFAULT_CONFIG) + def override(name, base = DEFAULT_CONFIG, &block) unless get(base) raise NotYetConfiguredError, "#{base} policy not yet supplied" end override = @configurations[base].dup - yield(override) + override.instance_eval &block if block_given? add_configuration(name, override) end @@ -99,7 +100,7 @@ def deep_copy_if_hash(value) end attr_writer :hsts, :x_frame_options, :x_content_type_options, - :x_xss_protection, :csp, :x_download_options, :x_permitted_cross_domain_policies, + :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :dynamic_csp, :secure_cookies attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies @@ -166,6 +167,14 @@ def validate_config! protected + def csp=(new_csp) + if self.dynamic_csp + raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= isntead." + end + + @csp = new_csp + end + def cached_headers=(headers) @cached_headers = headers end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 01556193..10fd4af2 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -36,7 +36,7 @@ module SecureHeaders config = Configuration.get(:test_override) noop = Configuration.get(Configuration::NOOP_CONFIGURATION) - [:csp, :dynamic_csp].each do |key| + [:csp, :dynamic_csp, :secure_cookies].each do |key| expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}." end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index c37f24c7..fa96515a 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -129,6 +129,24 @@ module SecureHeaders SecureHeaders.header_hash_for(request) end + it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do + default_config = Configuration.default + expect { default_config.csp = {} }.to raise_error(NoMethodError) + + # config is frozen + expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError) + + SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) + new_config = SecureHeaders.config_for(request) + expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError) + + expect do + new_config.instance_eval do + new_config.csp = {} + end + end.to raise_error(Configuration::IllegalPolicyModificationError) + end + it "overrides individual directives" do Configuration.default do |config| config.csp = { From 0d20f87006198a7fe1adac60217a4c2516757b19 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Mar 2016 09:16:29 -1000 Subject: [PATCH 182/636] ensure that dynamic policies are associated with the correct variations. this was a bug, but things still 'worked' externally --- .../headers/policy_management.rb | 5 +++-- spec/lib/secure_headers_spec.rb | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 672c065d..7ff03564 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -224,8 +224,9 @@ def combine_policies(original, additions) end def ua_to_variation(user_agent) - if family = user_agent.browser && VARIATIONS.key?(family) - VARIATIONS[family] + family = user_agent.browser + if family && VARIATIONS.key?(family) + family else OTHER end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index fa96515a..ffe73f22 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -61,6 +61,25 @@ module SecureHeaders expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN) end + it "produces a UA-specific CSP when overriding (and busting the cache)" do + config = Configuration.default do |config| + config.csp = { + default_src: %w('self'), + child_src: %w('self'), #unsupported by firefox + frame_src: %w('self') + } + end + firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox])) + + # append an unsupported directive + SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash)) + # append a supported directive + SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self')) + + hash = SecureHeaders.header_hash_for(firefox_request) + expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'") + end + it "produces a hash of headers with default config" do Configuration.default hash = SecureHeaders.header_hash_for(request) From bc31dc6e94ff0000ce8b804b0cb06f5134d9ce06 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Mar 2016 08:46:46 -1000 Subject: [PATCH 183/636] 3.1.0 version bump and changelog --- CHANGELOG.md | 10 ++++++++++ secure_headers.gemspec | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4c324f..809b4ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 3.1.0 Adding secure cookie support + +New feature: marking all cookies as secure. Added by @jmera in https://github.com/twitter/secureheaders/pull/231. In the future, we'll probably add the ability to whitelist individual cookies that should not be marked secure. PRs welcome. + +Internal refactoring: In https://github.com/twitter/secureheaders/pull/232, we changed the way dynamic CSP is handled internally. The biggest benefit is that highly dynamic policies (which can happen with multiple `append/override` calls per request) are handled better: + +1. Only the CSP header cache is busted when using a dynamic policy. All other headers are preserved and don't need to be generated. Dynamic X-Frame-Options changes modify the cache directly. +1. Idempotency checks for policy modifications are deferred until the end of the request lifecycle and only happen once, instead of per `append/override` call. The idempotency check itself is fairly expensive itself. +1. CSP header string is produced at most once per request. + ## 3.0.3 Bug fix for handling policy merges where appending a non-default source value (report-uri, plugin-types, frame-ancestors, base-uri, and form-action) would be combined with the default-src value. Appending a directive that doesn't exist in the current policy combines the new value with `default-src` to mimic the actual behavior of the addition. However, this does not make sense for non-default-src values (a.k.a. "fetch directives") and can lead to unexpected behavior like a `report-uri` value of `*`. Previously, this config: diff --git a/secure_headers.gemspec b/secure_headers.gemspec index ab29dc8e..6a50c6a5 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.0.3" + gem.version = "3.1.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 171ca586d8aac9928be7729868c4c24024c0912c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Mar 2016 10:20:57 -1000 Subject: [PATCH 184/636] add documentation on secure cookie feature --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index af73cd45..582123e2 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ The gem will automatically apply several headers that are related to security. - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) +It can also mark all http cookies with the secure attribute (when configured to do so). + `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings. ## Use @@ -29,6 +31,7 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` ```ruby SecureHeaders::Configuration.default do |config| + config.secure_cookies = true # mark all cookies as "secure" config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" From 0358c8e3e445509095b7daf510162dd2c663d4c4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Mar 2016 12:57:14 -1000 Subject: [PATCH 185/636] Opting out of all protection would raise an exception because the idempotency check was wrong --- lib/secure_headers/configuration.rb | 1 + lib/secure_headers/headers/policy_management.rb | 1 + spec/lib/secure_headers_spec.rb | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index f7a3ff16..f4154dc4 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -71,6 +71,7 @@ def add_noop_configuration ALL_HEADER_CLASSES.each do |klass| config.send("#{klass::CONFIG_KEY}=", OPT_OUT) end + config.dynamic_csp = OPT_OUT end add_configuration(NOOP_CONFIGURATION, noop_config) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 7ff03564..f78f5ffb 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -196,6 +196,7 @@ def validate_config!(config) # additions = { script_src: %w(google.com)} then idempotent_additions? would return # because google.com is already in the config. def idempotent_additions?(config, additions) + return true if config == OPT_OUT && additions == OPT_OUT return false if config == OPT_OUT config == combine_policies(config, additions) end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index ffe73f22..770ea39d 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -38,6 +38,7 @@ module SecureHeaders ALL_HEADER_CLASSES.each do |klass| expect(hash[klass::CONFIG_KEY]).to be_nil end + expect(hash.count).to eq(0) end it "allows you to override X-Frame-Options settings" do From cd56394fba1ebf2d60fa5594b13f1c0943c98a9a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Mar 2016 13:07:21 -1000 Subject: [PATCH 186/636] bump to 3.1.1 --- CHANGELOG.md | 10 ++++++++++ secure_headers.gemspec | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 809b4ea9..1c8d7af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 3.1.1 Bug fix for regression + +See https://github.com/twitter/secureheaders/pull/235 + +`idempotent_additions?` would return false when comparing `OPT_OUT` with `OPT_OUT`, causing `header_hash_for` to return a header cache with `{ nil => nil }` which cause the middleware to blow up when `{ nil => nil }` was merged into the rack header hash. + +This is a regression in 3.1.0 only. + +Now it returns true. I've added a test case to ensure that `header_hash_for` will never return such an element. + ## 3.1.0 Adding secure cookie support New feature: marking all cookies as secure. Added by @jmera in https://github.com/twitter/secureheaders/pull/231. In the future, we'll probably add the ability to whitelist individual cookies that should not be marked secure. PRs welcome. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 6a50c6a5..0f054d65 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.1.0" + gem.version = "3.1.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 4b4e510f88f9042b67d54df9c5f58b432be0ee8b Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 30 Mar 2016 13:37:42 -0400 Subject: [PATCH 187/636] add cookies configuration --- lib/secure_headers/configuration.rb | 5 +++-- spec/lib/secure_headers/configuration_spec.rb | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index f4154dc4..085ebd58 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -102,9 +102,9 @@ def deep_copy_if_hash(value) attr_writer :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, - :hpkp, :dynamic_csp, :secure_cookies + :hpkp, :dynamic_csp, :secure_cookies, :cookies - attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies + attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies, :cookies def initialize(&block) self.hpkp = OPT_OUT @@ -118,6 +118,7 @@ def initialize(&block) def dup copy = self.class.new copy.secure_cookies = @secure_cookies + copy.cookies = @cookies copy.csp = self.class.send(:deep_copy_if_hash, @csp) copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp) copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 10fd4af2..95e917ee 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -36,7 +36,7 @@ module SecureHeaders config = Configuration.get(:test_override) noop = Configuration.get(Configuration::NOOP_CONFIGURATION) - [:csp, :dynamic_csp, :secure_cookies].each do |key| + [:csp, :dynamic_csp, :secure_cookies, :cookies].each do |key| expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}." end end From 3c07b90a2de3a6c510c6639028b3d86e687c0d1c Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 30 Mar 2016 13:57:20 -0400 Subject: [PATCH 188/636] add support for Secure and HttpOnly cookies --- lib/secure_headers/middleware.rb | 47 +++++++++++-- spec/lib/secure_headers/middleware_spec.rb | 82 ++++++++++++++++++---- 2 files changed, 110 insertions(+), 19 deletions(-) diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 603359bc..771dcbae 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -1,6 +1,14 @@ +require 'cgi' + module SecureHeaders class Middleware SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze + HTTPONLY_COOKIE_REGEXP =/;\s*HttpOnly\s*(;|$)/i.freeze + + ATTRIBUTES = { + secure: "secure", + httponly: "HttpOnly", + } def initialize(app) @app = app @@ -12,7 +20,8 @@ def call(env) status, headers, response = @app.call(env) config = SecureHeaders.config_for(req) - flag_cookies_as_secure!(headers) if config.secure_cookies + flag_cookies!(headers, config.cookies) if config.cookies + flag_cookies!(headers, secure: true) if config.secure_cookies headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] end @@ -20,19 +29,43 @@ def call(env) private # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194 - def flag_cookies_as_secure!(headers) + def flag_cookies!(headers, config) if cookies = headers['Set-Cookie'] # Support Rails 2.3 / Rack 1.1 arrays as headers cookies = cookies.split("\n") unless cookies.is_a?(Array) headers['Set-Cookie'] = cookies.map do |cookie| - if cookie !~ SECURE_COOKIE_REGEXP - "#{cookie}; secure" - else - cookie - end + cookie = flag_cookie!(:secure, cookie, config, SECURE_COOKIE_REGEXP) + cookie = flag_cookie!(:httponly, cookie, config, HTTPONLY_COOKIE_REGEXP) end.join("\n") end end + + def flag_cookie!(attribute, cookie, config = false, regexp) + case config[attribute] + when NilClass, FalseClass + cookie + when TrueClass + flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) + when Hash + parsed_cookie = CGI::Cookie.parse(cookie) + + if((Array(config[attribute][:only]) & parsed_cookie.keys).any?) + flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) + elsif((Array(config[attribute][:except]) & parsed_cookie.keys).none?) + flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) + else + cookie + end + end + end + + def flag_unless_matches(cookie, attribute, regexp) + if cookie =~ regexp + cookie + else + "#{cookie}; #{attribute}" + end + end end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index c5d4b73b..6f1f131c 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -38,21 +38,79 @@ module SecureHeaders expect(env[CSP::HEADER_NAME]).to match("example.org") end - context "cookies should be flagged" do - it "flags cookies as secure" do - Configuration.default { |config| config.secure_cookies = true } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP) + context "secure_cookies" do + context "cookies should be flagged" do + it "flags cookies as secure" do + Configuration.default { |config| config.secure_cookies = true } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP) + end + end + + context "cookies should not be flagged" do + it "does not flags cookies as secure" do + Configuration.default { |config| config.secure_cookies = false } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).not_to match(Middleware::SECURE_COOKIE_REGEXP) + end end end - context "cookies should not be flagged" do - it "does not flags cookies as secure" do - Configuration.default { |config| config.secure_cookies = false } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).not_to match(Middleware::SECURE_COOKIE_REGEXP) + context "cookies" do + context "secure cookies" do + context "when secure is a boolean" do + it "flags cookies as secure" do + Configuration.default { |config| config.cookies = { secure: true } } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP) + end + end + + context "when secure is a Hash" do + it "flags cookies as secure when whitelisted" do + Configuration.default { |config| config.cookies = { secure: { only: ['foo']} } } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP) + end + + it "does not flag cookies as secure when excluded" do + Configuration.default { |config| config.cookies = { secure: { except: ['foo']} } } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).not_to match(Middleware::SECURE_COOKIE_REGEXP) + end + end + end + + context "HttpOnly cookies" do + context "when httponly is a boolean" do + it "flags cookies as HttpOnly" do + Configuration.default { |config| config.cookies = { httponly: true } } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).to match(Middleware::HTTPONLY_COOKIE_REGEXP) + end + end + + context "when secure is a Hash" do + it "flags cookies as secure when whitelisted" do + Configuration.default { |config| config.cookies = { httponly: { only: ['foo']} } } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).to match(Middleware::HTTPONLY_COOKIE_REGEXP) + end + + it "does not flag cookies as secure when excluded" do + Configuration.default { |config| config.cookies = { httponly: { except: ['foo']} } } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).not_to match(Middleware::HTTPONLY_COOKIE_REGEXP) + end + end end end end From 34c8129356f96d6f2bfd5c2311008c76d08077fe Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 30 Mar 2016 09:34:39 -1000 Subject: [PATCH 189/636] copy forward values so header cache regeneration carries forward overrides --- lib/secure_headers/configuration.rb | 8 ++++++++ spec/lib/secure_headers/configuration_spec.rb | 8 ++++++++ spec/lib/secure_headers_spec.rb | 19 ++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index f4154dc4..0fca4e01 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -121,6 +121,13 @@ def dup copy.csp = self.class.send(:deep_copy_if_hash, @csp) copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp) copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) + copy.x_content_type_options = @x_content_type_options + copy.hsts = @hsts + copy.x_frame_options = @x_frame_options + copy.x_xss_protection = @x_xss_protection + copy.x_download_options = @x_download_options + copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies + copy.hpkp = @hpkp copy end @@ -133,6 +140,7 @@ def opt_out(header) end def update_x_frame_options(value) + @x_frame_options = value self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value) end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 10fd4af2..24efdbbe 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -41,6 +41,14 @@ module SecureHeaders end end + it "regenerates cached headers when building an override" do + Configuration.override(:test_override) do |config| + config.x_content_type_options = OPT_OUT + end + + expect(Configuration.get.cached_headers).to_not eq(Configuration.get(:test_override).cached_headers) + end + it "stores an override of the global config" do Configuration.override(:test_override) do |config| config.x_frame_options = "DENY" diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 770ea39d..db956e83 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -21,7 +21,7 @@ module SecureHeaders end describe "#header_hash_for" do - it "allows you to opt out of individual headers" do + it "allows you to opt out of individual headers via API" do Configuration.default SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY) SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY) @@ -31,6 +31,23 @@ module SecureHeaders expect(hash['X-Content-Type-Options']).to be_nil end + it "Carries options over when using overrides" do + Configuration.default do |config| + config.x_download_options = OPT_OUT + config.x_permitted_cross_domain_policies = OPT_OUT + end + + Configuration.override(:api) do |config| + config.x_frame_options = OPT_OUT + end + + SecureHeaders.use_secure_headers_override(request, :api) + hash = SecureHeaders.header_hash_for(request) + expect(hash['X-Download-Options']).to be_nil + expect(hash['X-Permitted-Cross-Domain-Policies']).to be_nil + expect(hash['X-Frame-Options']).to be_nil + end + it "allows you to opt out entirely" do Configuration.default SecureHeaders.opt_out_of_all_protection(request) From 83a564a235c8be1a8a3901373dbc769da32f6ed7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 30 Mar 2016 09:56:08 -1000 Subject: [PATCH 190/636] version bump to 3.1.2 --- CHANGELOG.md | 6 ++++++ secure_headers.gemspec | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8d7af0..2b050d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.1.2 Bug fix for regression + +See https://github.com/twitter/secureheaders/pull/239 + +This meant that when header caches were regenerated upon calling `SecureHeaders.override(:name)` and using it with `use_secure_headers_override` would result in default values for anything other than CSP/HPKP. + ## 3.1.1 Bug fix for regression See https://github.com/twitter/secureheaders/pull/235 diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 0f054d65..af75980d 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.1.1" + gem.version = "3.1.2" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 6789cd47e713e14565722a133873a903f46d115f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 30 Mar 2016 15:28:57 -1000 Subject: [PATCH 191/636] re-add support for script hashes --- lib/secure_headers.rb | 1 + lib/secure_headers/configuration.rb | 8 +++++ lib/secure_headers/hash_helper.rb | 7 +++++ lib/secure_headers/railtie.rb | 4 +++ lib/secure_headers/view_helper.rb | 38 +++++++++++++++++++++++ lib/tasks/tasks.rake | 48 +++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+) create mode 100644 lib/secure_headers/hash_helper.rb create mode 100644 lib/tasks/tasks.rake diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 8a7e2cee..771f5bf2 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -1,4 +1,5 @@ require "secure_headers/configuration" +require "secure_headers/hash_helper" require "secure_headers/headers/public_key_pins" require "secure_headers/headers/content_security_policy" require "secure_headers/headers/x_frame_options" diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 0fca4e01..5ae78b27 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -1,3 +1,5 @@ +require 'yaml' + module SecureHeaders class Configuration DEFAULT_CONFIG = :default @@ -106,6 +108,12 @@ def deep_copy_if_hash(value) attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies + + SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml' + if File.exists?(SCRIPT_HASH_CONFIG_FILE) + @script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE)) + end + def initialize(&block) self.hpkp = OPT_OUT self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG) diff --git a/lib/secure_headers/hash_helper.rb b/lib/secure_headers/hash_helper.rb new file mode 100644 index 00000000..35c6b2d9 --- /dev/null +++ b/lib/secure_headers/hash_helper.rb @@ -0,0 +1,7 @@ +module SecureHeaders + module HashHelper + def hash_source(inline_script, digest = :SHA256) + ["'", [digest.to_s.downcase, "-", [[Digest.const_get(digest).hexdigest(inline_script)].pack("H*")].pack("m").chomp].join, "'"].join + end + end +end diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index efa80be2..186bc475 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -13,6 +13,10 @@ class Railtie < Rails::Railtie Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware end + rake_tasks do + load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__)) + end + initializer "secure_headers.action_controller" do ActiveSupport.on_load(:action_controller) do include SecureHeaders diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index b5513d7d..dedbb9f7 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -1,5 +1,10 @@ module SecureHeaders module ViewHelpers + include SecureHeaders::HashHelper + SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes" + + class UnexpectedHashedScriptException < StandardError; end + # Public: create a style tag using the content security policy nonce. # Instructs secure_headers to append a nonce to style/script-src directives. # @@ -29,8 +34,41 @@ def content_security_policy_nonce(type) end end + def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block) + if raise_error_on_unrecognized_hash.nil? + raise_error_on_unrecognized_hash = !['development', 'test'].include?(ENV["RAILS_ENV"]) + end + content = capture(&block) + + hash_value = hash_source(content) + file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb') + script_hashes = Configuration.instance_variable_get(:@script_hashes)[file_path] + unless script_hashes && script_hashes.include?(hash_value) + message = unexpected_hash_error_message(file_path, hash_value, content) + if raise_error_on_unrecognized_hash + raise UnexpectedHashedScriptException.new(message) + else + warn message + end + end + + SecureHeaders.append_content_security_policy_directives(request, script_src: [hash_value]) + + content_tag :script, content + end + private + def unexpected_hash_error_message(file_path, hash_value, content) + <<-EOF +\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} *** + +*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:*** +#{file_path}: +- #{hash_value}\n\n + EOF + end + def nonced_tag(type, content_or_options, block) options = {} content = if block diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake new file mode 100644 index 00000000..8ca10bab --- /dev/null +++ b/lib/tasks/tasks.rake @@ -0,0 +1,48 @@ +INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx +INLINE_HASH_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx +SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml' + +namespace :secure_headers do + include SecureHeaders::HashHelper + + def is_erb?(filename) + filename =~ /\.erb\Z/ + end + + def generate_inline_script_hashes(filename) + file = File.read(filename) + hashes = [] + + [INLINE_SCRIPT_REGEX, INLINE_HASH_HELPER_REGEX].each do |regex| + file.gsub(regex) do # TODO don't use gsub + inline_script = Regexp.last_match.captures.last + if (filename =~ /\.mustache\Z/ && inline_script =~ /\{\{.*\}\}/) || (is_erb?(filename) && inline_script =~ /<%.*%>/) + puts "Looks like there's some dynamic content inside of a script tag :-/" + puts "That pretty much means the hash value will never match." + puts "Code: " + inline_script + puts "=" * 20 + end + + hashes << hash_source(inline_script) + end + end + + hashes + end + + task :generate_hashes do |t, args| + script_hashes = {} + Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename| + hashes = generate_inline_script_hashes(filename) + if hashes.any? + script_hashes[filename] = hashes + end + end + + File.open(SCRIPT_HASH_CONFIG_FILE, 'w') do |file| + file.write(script_hashes.to_yaml) + end + + puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SCRIPT_HASH_CONFIG_FILE}" + end +end From 6895207ac3da7dd362abcab5791a5b85e2cfb1a8 Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 31 Mar 2016 09:09:26 -0400 Subject: [PATCH 192/636] add support for SameSite cookies when passed a boolean --- lib/secure_headers/middleware.rb | 22 ++++++++++++++++++++++ spec/lib/secure_headers/middleware_spec.rb | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 771dcbae..fc011318 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -4,10 +4,12 @@ module SecureHeaders class Middleware SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze HTTPONLY_COOKIE_REGEXP =/;\s*HttpOnly\s*(;|$)/i.freeze + SAMESITE_COOKIE_REGEXP =/;\s*SameSite\s*(;|$)/i.freeze ATTRIBUTES = { secure: "secure", httponly: "HttpOnly", + samesite: "SameSite" } def initialize(app) @@ -37,6 +39,7 @@ def flag_cookies!(headers, config) headers['Set-Cookie'] = cookies.map do |cookie| cookie = flag_cookie!(:secure, cookie, config, SECURE_COOKIE_REGEXP) cookie = flag_cookie!(:httponly, cookie, config, HTTPONLY_COOKIE_REGEXP) + cookie = flag_samesite_cookie!(:samesite, cookie, config) end.join("\n") end end @@ -60,6 +63,25 @@ def flag_cookie!(attribute, cookie, config = false, regexp) end end + def flag_samesite_cookie!(attribute, cookie, config = false) + case config[attribute] + when NilClass, FalseClass + cookie + when TrueClass + flag_unless_matches(cookie, ATTRIBUTES[attribute], SAMESITE_COOKIE_REGEXP) + when Hash + parsed_cookie = CGI::Cookie.parse(cookie) + + if((Array(config[attribute][:only]) & parsed_cookie.keys).any?) + flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) + elsif((Array(config[attribute][:except]) & parsed_cookie.keys).none?) + flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) + else + cookie + end + end + end + def flag_unless_matches(cookie, attribute, regexp) if cookie =~ regexp cookie diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 6f1f131c..b55c3be5 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -112,6 +112,17 @@ module SecureHeaders end end end + + context "SameSite cookies" do + context "when samesite is a boolean" do + it "flags cookies as Samesite" do + Configuration.default { |config| config.cookies = { samesite: true } } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).to match(Middleware::SAMESITE_COOKIE_REGEXP) + end + end + end end end end From 8f8053b273670b423ae5ede4fa0b0178e32ab81b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 1 Apr 2016 11:02:19 -1000 Subject: [PATCH 193/636] add style hash integration /cc @reedloden --- lib/secure_headers/configuration.rb | 9 ++-- lib/secure_headers/view_helper.rb | 56 ++++++++++++++++++----- lib/tasks/tasks.rake | 71 +++++++++++++++++++++-------- 3 files changed, 101 insertions(+), 35 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 5ae78b27..f92d3354 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -108,10 +108,11 @@ def deep_copy_if_hash(value) attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies - - SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml' - if File.exists?(SCRIPT_HASH_CONFIG_FILE) - @script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE)) + HASH_CONFIG_FILE = 'config/secure_headers_generated_hashes.yml' + if File.exists?(HASH_CONFIG_FILE) + config = YAML.safe_load(File.open(HASH_CONFIG_FILE)) + @script_hashes = config["scripts"] + @style_hashes = config["styles"] end def initialize(&block) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index dedbb9f7..913949cf 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -34,17 +34,48 @@ def content_security_policy_nonce(type) end end + ## + # Checks to see if the hashed code is expected and adds the hash source + # value to the current CSP. + # + # In development/test/etc. an exception will be raised. In production, + # it will dynamically compute the value so things don't break. def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block) + hashed_tag( + :script, + :script_src, + Configuration.instance_variable_get(:@script_hashes), + raise_error_on_unrecognized_hash, + block + ) + end + + def hashed_style_tag(raise_error_on_unrecognized_hash = nil, &block) + hashed_tag( + :style, + :style_src, + Configuration.instance_variable_get(:@style_hashes), + raise_error_on_unrecognized_hash, + block + ) + end + + private + + def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block) if raise_error_on_unrecognized_hash.nil? - raise_error_on_unrecognized_hash = !['development', 'test'].include?(ENV["RAILS_ENV"]) + raise_error_on_unrecognized_hash = ENV["RAILS_ENV"] != "production" end - content = capture(&block) - hash_value = hash_source(content) + content = capture(&block) file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb') - script_hashes = Configuration.instance_variable_get(:@script_hashes)[file_path] - unless script_hashes && script_hashes.include?(hash_value) - message = unexpected_hash_error_message(file_path, hash_value, content) + + if hashes.nil? + raise UnexpectedHashedScriptException.new(unexpected_hash_error_message(file_path, content)) + end + + if hashes[file_path].nil? + message = unexpected_hash_error_message(file_path, content) if raise_error_on_unrecognized_hash raise UnexpectedHashedScriptException.new(message) else @@ -52,20 +83,21 @@ def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block) end end - SecureHeaders.append_content_security_policy_directives(request, script_src: [hash_value]) + SecureHeaders.append_content_security_policy_directives(request, directive => [hashes[file_path]]) - content_tag :script, content + content_tag type, content end - private - - def unexpected_hash_error_message(file_path, hash_value, content) + def unexpected_hash_error_message(file_path, content) + hash_value = hash_source(content) <<-EOF \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} *** - +#{content} *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:*** #{file_path}: - #{hash_value}\n\n + NOTE: dynamic javascript is not supported using script hash integration + on purpose. It defeats the point of using it in the first place. EOF end diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake index 8ca10bab..4af1c051 100644 --- a/lib/tasks/tasks.rake +++ b/lib/tasks/tasks.rake @@ -1,6 +1,7 @@ INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx -INLINE_HASH_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx -SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml' +INLINE_STYLE_REGEX = /(]*>)(.*?)<\/style>/mx +INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx +INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx namespace :secure_headers do include SecureHeaders::HashHelper @@ -9,40 +10,72 @@ namespace :secure_headers do filename =~ /\.erb\Z/ end - def generate_inline_script_hashes(filename) + def is_mustache?(filename) + filename =~ /\.mustache\Z/ + end + + def dynamic_content?(filename, inline_script) + (is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) || + (is_erb?(filename) && inline_script =~ /<%.*%>/) + end + + def find_inline_content(filename, regex, hashes) file = File.read(filename) + file.scan(regex) do # TODO don't use gsub + inline_script = Regexp.last_match.captures.last + if dynamic_content?(filename, inline_script) + puts "Looks like there's some dynamic content inside of a tag :-/" + puts "That pretty much means the hash value will never match." + puts "Code: " + inline_script + puts "=" * 20 + end + + hashes << hash_source(inline_script) + end + end + + def generate_inline_script_hashes(filename) hashes = [] - [INLINE_SCRIPT_REGEX, INLINE_HASH_HELPER_REGEX].each do |regex| - file.gsub(regex) do # TODO don't use gsub - inline_script = Regexp.last_match.captures.last - if (filename =~ /\.mustache\Z/ && inline_script =~ /\{\{.*\}\}/) || (is_erb?(filename) && inline_script =~ /<%.*%>/) - puts "Looks like there's some dynamic content inside of a script tag :-/" - puts "That pretty much means the hash value will never match." - puts "Code: " + inline_script - puts "=" * 20 - end - - hashes << hash_source(inline_script) - end + [INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex| + find_inline_content(filename, regex, hashes) + end + + hashes + end + + def generate_inline_style_hashes(filename) + hashes = [] + + [INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex| + find_inline_content(filename, regex, hashes) end hashes end task :generate_hashes do |t, args| - script_hashes = {} + script_hashes = { + "scripts" => {}, + "styles" => {} + } + Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename| hashes = generate_inline_script_hashes(filename) if hashes.any? - script_hashes[filename] = hashes + script_hashes["scripts"][filename] = hashes + end + + hashes = generate_inline_style_hashes(filename) + if hashes.any? + script_hashes["styles"][filename] = hashes end end - File.open(SCRIPT_HASH_CONFIG_FILE, 'w') do |file| + File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file| file.write(script_hashes.to_yaml) end - puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SCRIPT_HASH_CONFIG_FILE}" + puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" end end From 6b5324ff393d6d89bc24a9f5c6cfe2da4c5860a8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 1 Apr 2016 11:06:01 -1000 Subject: [PATCH 194/636] make hash_source method less confusing --- lib/secure_headers/hash_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/hash_helper.rb b/lib/secure_headers/hash_helper.rb index 35c6b2d9..0b983540 100644 --- a/lib/secure_headers/hash_helper.rb +++ b/lib/secure_headers/hash_helper.rb @@ -1,7 +1,8 @@ module SecureHeaders module HashHelper def hash_source(inline_script, digest = :SHA256) - ["'", [digest.to_s.downcase, "-", [[Digest.const_get(digest).hexdigest(inline_script)].pack("H*")].pack("m").chomp].join, "'"].join + base64_hashed_content = Digest.const_get(digest).hexdigest(inline_script)].pack("H*")].pack("m").chomp + "'#{digest.to_s.downcase}-#{base64_hashed_content}'" end end end From 8e18257bffbb47d76ae6902c85e15d3d2198d454 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 1 Apr 2016 13:08:16 -1000 Subject: [PATCH 195/636] oops, hash_source method regressed --- lib/secure_headers/hash_helper.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/hash_helper.rb b/lib/secure_headers/hash_helper.rb index 0b983540..9f591709 100644 --- a/lib/secure_headers/hash_helper.rb +++ b/lib/secure_headers/hash_helper.rb @@ -1,7 +1,9 @@ +require 'base64' + module SecureHeaders module HashHelper def hash_source(inline_script, digest = :SHA256) - base64_hashed_content = Digest.const_get(digest).hexdigest(inline_script)].pack("H*")].pack("m").chomp + base64_hashed_content = Base64.encode64(Digest.const_get(digest).digest(inline_script)).chomp "'#{digest.to_s.downcase}-#{base64_hashed_content}'" end end From 5c24b7edfde210e29889741fe2e67d58ee777d86 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 1 Apr 2016 14:12:10 -1000 Subject: [PATCH 196/636] add test for view helpers --- spec/lib/secure_headers/middleware_spec.rb | 4 +- spec/lib/secure_headers/view_helpers_spec.rb | 101 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 spec/lib/secure_headers/view_helpers_spec.rb diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index c5d4b73b..7ccb8388 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -10,9 +10,7 @@ module SecureHeaders before(:each) do reset_config - Configuration.default do |config| - # use all default provided by the library - end + Configuration.default end it "sets the headers" do diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb new file mode 100644 index 00000000..4fa40966 --- /dev/null +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -0,0 +1,101 @@ +require "spec_helper" +require "erb" + +class Message < ERB + include SecureHeaders::ViewHelpers + + def self.template +<<-TEMPLATE +<% hashed_javascript_tag do %> + console.log(1) +<% end %> + +<% hashed_style_tag do %> + body { + background-color: black; + } +<% end %> + +<% nonced_javascript_tag do %> + body { + console.log(1) + } +<% end %> + +<% nonced_style_tag do %> + body { + background-color: black; + } +<% end %> +<%= @name %> + +TEMPLATE + end + + def initialize(request, options = {}) + @virtual_path = "/asdfs/index" + @_request = request + @template = self.class.template + super(@template) + end + + def capture(*args) + yield(*args) + end + + def content_tag(type, content = nil, options = nil, &block) + content = if block_given? + capture(block) + end + + if options.is_a?(Hash) + options = options.map {|k,v| " #{k}=#{v}"} + end + "<#{type}#{options}>#{content}" + end + + def result + super(binding) + end + + def request + @_request + end +end + +module SecureHeaders + describe ViewHelpers do + let(:app) { lambda { |env| [200, env, "app"] } } + let(:middleware) { Middleware.new(app) } + + it "uses view helpers" do + begin + allow(SecureRandom).to receive(:base64).and_return("abc123") + + Configuration.override("my_custom_config") do |config| + config.csp[:script_src] = %w('self') + config.csp[:style_src] = %w('self') + end + request = Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) + SecureHeaders.use_secure_headers_override(request, "my_custom_config") + + expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8=" + Configuration.instance_variable_set(:@script_hashes, "app/views/asdfs/index.html.erb" => ["'#{expected_hash}'"]) + expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc=" + Configuration.instance_variable_set(:@style_hashes, "app/views/asdfs/index.html.erb" => ["'#{expected_style_hash}'"]) + + # render erb that calls out to helpers. + Message.new(request).result + _, env = middleware.call request.env + + expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/) + expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/) + expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/) + expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) + ensure + Configuration.instance_variable_set(:@script_hashes, nil) + Configuration.instance_variable_set(:@style_hashes, nil) + end + end + end +end From 5518d351c11ae5a90315fa8d91edf8b822ea7ca2 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 1 Apr 2016 14:14:58 -1000 Subject: [PATCH 197/636] because hashes are already an array --- lib/secure_headers/view_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 913949cf..70088d05 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -83,7 +83,7 @@ def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block) end end - SecureHeaders.append_content_security_policy_directives(request, directive => [hashes[file_path]]) + SecureHeaders.append_content_security_policy_directives(request, directive => hashes[file_path]) content_tag type, content end From cff9a9ec3acd3ac11817957ec03bd1ae64b5c32e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 1 Apr 2016 14:21:40 -1000 Subject: [PATCH 198/636] default may not have been called at this point --- spec/lib/secure_headers/view_helpers_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 4fa40966..ca05a927 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -72,7 +72,7 @@ module SecureHeaders begin allow(SecureRandom).to receive(:base64).and_return("abc123") - Configuration.override("my_custom_config") do |config| + Configuration.default("my_custom_config") do |config| config.csp[:script_src] = %w('self') config.csp[:style_src] = %w('self') end From 6d2f92ff2d52ea2996bd84a9ddd248a2a79595ff Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 1 Apr 2016 14:24:47 -1000 Subject: [PATCH 199/636] use correct API for default --- spec/lib/secure_headers/view_helpers_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index ca05a927..c62b94ab 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -72,12 +72,11 @@ module SecureHeaders begin allow(SecureRandom).to receive(:base64).and_return("abc123") - Configuration.default("my_custom_config") do |config| + Configuration.default do |config| config.csp[:script_src] = %w('self') config.csp[:style_src] = %w('self') end request = Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) - SecureHeaders.use_secure_headers_override(request, "my_custom_config") expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8=" Configuration.instance_variable_set(:@script_hashes, "app/views/asdfs/index.html.erb" => ["'#{expected_hash}'"]) From 1fb498531c8d84ce76421656538ca862c3ce15e6 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 4 Apr 2016 09:36:53 -1000 Subject: [PATCH 200/636] Update docs for proper nonce use /cc @dqdinh and https://github.com/twitter/secureheaders/issues/242 --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 582123e2..dc982b34 100644 --- a/README.md +++ b/README.md @@ -195,24 +195,6 @@ Code | Result #### Nonce -script/style-nonce can be used to whitelist inline content. To do this, call the `SecureHeaders.content_security_policy_nonce` then set the nonce attributes on the various tags. - -Setting a nonce will also set 'unsafe-inline' for browsers that don't support nonces for backwards compatibility. 'unsafe-inline' is ignored if a nonce is present in a directive in compliant browsers. - -```erb - - - - - -``` - You can use a view helper to automatically add nonces to script tags: ```erb @@ -240,6 +222,24 @@ body { ``` +script/style-nonce can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags. + +Setting a nonce will also set 'unsafe-inline' for browsers that don't support nonces for backwards compatibility. 'unsafe-inline' is ignored if a nonce is present in a directive in compliant browsers. + +```erb + + + + + +``` + #### Hash The hash feature has been removed, for now. From 9739fc4d19718e5c8694d3222b52c91ccceffc9b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 4 Apr 2016 11:17:41 -1000 Subject: [PATCH 201/636] Update upgrading-to-3-0.md --- upgrading-to-3-0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/upgrading-to-3-0.md b/upgrading-to-3-0.md index b65b418f..0317a796 100644 --- a/upgrading-to-3-0.md +++ b/upgrading-to-3-0.md @@ -8,6 +8,7 @@ Changes | Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block | | All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) | | CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings | +| CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_script_nonce` or `content_security_policy_style_nonce` | `self`/`none` source expressions | could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` | | `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | | Per-action configuration | override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) | From 4aaac0ce242d31459a4dc7d602070c085a6f2f81 Mon Sep 17 00:00:00 2001 From: stve Date: Mon, 4 Apr 2016 23:15:36 -0400 Subject: [PATCH 202/636] refactor cookie logic from middleware to cookie class --- lib/secure_headers.rb | 1 + lib/secure_headers/cookie.rb | 78 ++++++++++++++++++++++ lib/secure_headers/middleware.rb | 62 +---------------- spec/lib/secure_headers/cookie_spec.rb | 27 ++++++++ spec/lib/secure_headers/middleware_spec.rb | 22 +++--- 5 files changed, 120 insertions(+), 70 deletions(-) create mode 100644 lib/secure_headers/cookie.rb create mode 100644 spec/lib/secure_headers/cookie_spec.rb diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 8a7e2cee..588b7677 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -1,4 +1,5 @@ require "secure_headers/configuration" +require "secure_headers/cookie" require "secure_headers/headers/public_key_pins" require "secure_headers/headers/content_security_policy" require "secure_headers/headers/x_frame_options" diff --git a/lib/secure_headers/cookie.rb b/lib/secure_headers/cookie.rb new file mode 100644 index 00000000..0446e647 --- /dev/null +++ b/lib/secure_headers/cookie.rb @@ -0,0 +1,78 @@ +require 'cgi' + +module SecureHeaders + class Cookie + SECURE_REGEXP = /;\s*secure\s*(;|$)/i.freeze + HTTPONLY_REGEXP =/;\s*HttpOnly\s*(;|$)/i.freeze + SAMESITE_REGEXP =/;\s*SameSite\s*(;|$)/i.freeze + + REGEXES = { + secure: SECURE_REGEXP, + httponly: HTTPONLY_REGEXP, + samesite: SAMESITE_REGEXP, + } + + attr_reader :raw_cookie, :config + + def initialize(cookie, config) + @raw_cookie = cookie + @config = config + end + + def to_s + @raw_cookie.dup.tap do |c| + c << "; secure" if secure? + c << "; HttpOnly" if httponly? + c << "; SameSite" if samesite? + end + end + + def secure? + already_flagged?(:secure) || flag_cookie?(:secure) + end + + def httponly? + already_flagged?(:httponly) || flag_cookie?(:httponly) + end + + def samesite? + already_flagged?(:samesite) || flag_samesite? + end + + private + + def parsed_cookie + @parsed_cookie ||= CGI::Cookie.parse(raw_cookie) + end + + def already_flagged?(attribute) + raw_cookie =~ REGEXES[attribute] + end + + def flag_cookie?(attribute) + case config[attribute] + when TrueClass + true + when Hash + if((Array(config[attribute][:only]) & parsed_cookie.keys).any?) + true + elsif((Array(config[attribute][:except]) & parsed_cookie.keys).none?) + true + else + false + end + else + false + end + end + + def flag_samesite? + case config[:samesite] + when TrueClass + true + else + false + end + end + end +end diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index fc011318..85f7364b 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -1,17 +1,5 @@ -require 'cgi' - module SecureHeaders class Middleware - SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze - HTTPONLY_COOKIE_REGEXP =/;\s*HttpOnly\s*(;|$)/i.freeze - SAMESITE_COOKIE_REGEXP =/;\s*SameSite\s*(;|$)/i.freeze - - ATTRIBUTES = { - secure: "secure", - httponly: "HttpOnly", - samesite: "SameSite" - } - def initialize(app) @app = app end @@ -37,57 +25,9 @@ def flag_cookies!(headers, config) cookies = cookies.split("\n") unless cookies.is_a?(Array) headers['Set-Cookie'] = cookies.map do |cookie| - cookie = flag_cookie!(:secure, cookie, config, SECURE_COOKIE_REGEXP) - cookie = flag_cookie!(:httponly, cookie, config, HTTPONLY_COOKIE_REGEXP) - cookie = flag_samesite_cookie!(:samesite, cookie, config) + SecureHeaders::Cookie.new(cookie, config).to_s end.join("\n") end end - - def flag_cookie!(attribute, cookie, config = false, regexp) - case config[attribute] - when NilClass, FalseClass - cookie - when TrueClass - flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) - when Hash - parsed_cookie = CGI::Cookie.parse(cookie) - - if((Array(config[attribute][:only]) & parsed_cookie.keys).any?) - flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) - elsif((Array(config[attribute][:except]) & parsed_cookie.keys).none?) - flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) - else - cookie - end - end - end - - def flag_samesite_cookie!(attribute, cookie, config = false) - case config[attribute] - when NilClass, FalseClass - cookie - when TrueClass - flag_unless_matches(cookie, ATTRIBUTES[attribute], SAMESITE_COOKIE_REGEXP) - when Hash - parsed_cookie = CGI::Cookie.parse(cookie) - - if((Array(config[attribute][:only]) & parsed_cookie.keys).any?) - flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) - elsif((Array(config[attribute][:except]) & parsed_cookie.keys).none?) - flag_unless_matches(cookie, ATTRIBUTES[attribute], regexp) - else - cookie - end - end - end - - def flag_unless_matches(cookie, attribute, regexp) - if cookie =~ regexp - cookie - else - "#{cookie}; #{attribute}" - end - end end end diff --git a/spec/lib/secure_headers/cookie_spec.rb b/spec/lib/secure_headers/cookie_spec.rb new file mode 100644 index 00000000..51e5c9e0 --- /dev/null +++ b/spec/lib/secure_headers/cookie_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +module SecureHeaders + describe Cookie do + let(:raw_cookie) { "_session=thisisatest" } + + it "does not tamper with cookies when unconfigured" do + cookie = Cookie.new(raw_cookie, {}) + expect(cookie.to_s).to eq(raw_cookie) + end + + it "flags secure cookies" do + cookie = Cookie.new(raw_cookie, secure: true) + expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) + end + + it "flags HttpOnly cookies" do + cookie = Cookie.new(raw_cookie, httponly: true) + expect(cookie.to_s).to match(Cookie::HTTPONLY_REGEXP) + end + + it "flags SameSite cookies" do + cookie = Cookie.new(raw_cookie, samesite: true) + expect(cookie.to_s).to match(Cookie::SAMESITE_REGEXP) + end + end +end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index b55c3be5..c44589dd 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -44,7 +44,7 @@ module SecureHeaders Configuration.default { |config| config.secure_cookies = true } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) end end @@ -53,7 +53,7 @@ module SecureHeaders Configuration.default { |config| config.secure_cookies = false } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).not_to match(Middleware::SECURE_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) end end end @@ -65,7 +65,7 @@ module SecureHeaders Configuration.default { |config| config.cookies = { secure: true } } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) end end @@ -74,14 +74,14 @@ module SecureHeaders Configuration.default { |config| config.cookies = { secure: { only: ['foo']} } } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) end it "does not flag cookies as secure when excluded" do Configuration.default { |config| config.cookies = { secure: { except: ['foo']} } } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).not_to match(Middleware::SECURE_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) end end end @@ -92,7 +92,7 @@ module SecureHeaders Configuration.default { |config| config.cookies = { httponly: true } } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).to match(Middleware::HTTPONLY_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) end end @@ -101,14 +101,14 @@ module SecureHeaders Configuration.default { |config| config.cookies = { httponly: { only: ['foo']} } } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).to match(Middleware::HTTPONLY_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) end it "does not flag cookies as secure when excluded" do Configuration.default { |config| config.cookies = { httponly: { except: ['foo']} } } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).not_to match(Middleware::HTTPONLY_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).not_to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) end end end @@ -119,9 +119,13 @@ module SecureHeaders Configuration.default { |config| config.cookies = { samesite: true } } request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' - expect(response.headers['Set-Cookie']).to match(Middleware::SAMESITE_COOKIE_REGEXP) + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SAMESITE_REGEXP) end end + + context "when samesite is a Hash" do + skip + end end end end From 5cd8dbcfdaead117906a33dbfe70be8b2022517f Mon Sep 17 00:00:00 2001 From: stve Date: Tue, 5 Apr 2016 16:55:08 -0400 Subject: [PATCH 203/636] deprecate Configuration#secure_cookies= usage Raises a deprecation warning and merges { secure: true } into the standard cookies configuration. --- lib/secure_headers/configuration.rb | 10 +++++++--- lib/secure_headers/middleware.rb | 1 - spec/lib/secure_headers/configuration_spec.rb | 10 +++++++++- spec/lib/secure_headers/middleware_spec.rb | 8 ++++++-- spec/spec_helper.rb | 12 ++++++++++++ 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 085ebd58..52dc78c4 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -102,9 +102,9 @@ def deep_copy_if_hash(value) attr_writer :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, - :hpkp, :dynamic_csp, :secure_cookies, :cookies + :hpkp, :dynamic_csp, :cookies - attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies, :cookies + attr_reader :cached_headers, :csp, :dynamic_csp, :cookies def initialize(&block) self.hpkp = OPT_OUT @@ -117,7 +117,6 @@ def initialize(&block) # Returns a deep-dup'd copy of this configuration. def dup copy = self.class.new - copy.secure_cookies = @secure_cookies copy.cookies = @cookies copy.csp = self.class.send(:deep_copy_if_hash, @csp) copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp) @@ -167,6 +166,11 @@ def validate_config! PublicKeyPins.validate_config!(@hpkp) end + def secure_cookies=(secure_cookies) + Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead." + @cookies = (@cookies || {}).merge(secure: secure_cookies) + end + protected def csp=(new_csp) diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 85f7364b..ecd11f97 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -11,7 +11,6 @@ def call(env) config = SecureHeaders.config_for(req) flag_cookies!(headers, config.cookies) if config.cookies - flag_cookies!(headers, secure: true) if config.secure_cookies headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 95e917ee..a4bfab73 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -36,7 +36,7 @@ module SecureHeaders config = Configuration.get(:test_override) noop = Configuration.get(Configuration::NOOP_CONFIGURATION) - [:csp, :dynamic_csp, :secure_cookies, :cookies].each do |key| + [:csp, :dynamic_csp, :cookies].each do |key| expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}." end end @@ -74,5 +74,13 @@ module SecureHeaders override_config = Configuration.get(:second_override) expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org)) end + + it "deprecates the secure_cookies configuration" do + expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/) + + Configuration.default do |config| + config.secure_cookies = true + end + end end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index c44589dd..033e795f 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -41,7 +41,9 @@ module SecureHeaders context "secure_cookies" do context "cookies should be flagged" do it "flags cookies as secure" do - Configuration.default { |config| config.secure_cookies = true } + capture_warning do + Configuration.default { |config| config.secure_cookies = true } + end request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) @@ -50,7 +52,9 @@ module SecureHeaders context "cookies should not be flagged" do it "does not flags cookies as secure" do - Configuration.default { |config| config.secure_cookies = false } + capture_warning do + Configuration.default { |config| config.secure_cookies = false } + end request = Rack::MockRequest.new(cookie_middleware) response = request.get '/' expect(response.headers['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5e02e0ba..b74b1ca8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -45,3 +45,15 @@ def clear_configurations def reset_config SecureHeaders::Configuration.clear_configurations end + +def capture_warning + begin + old_stderr = $stderr + $stderr = StringIO.new + yield + result = $stderr.string + ensure + $stderr = old_stderr + end + result +end From 2ea4f047e1d37e5f95b42d3387b08d21913407c3 Mon Sep 17 00:00:00 2001 From: stve Date: Tue, 5 Apr 2016 16:57:00 -0400 Subject: [PATCH 204/636] fix typo in error message --- lib/secure_headers/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 52dc78c4..c31671bc 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -175,7 +175,7 @@ def secure_cookies=(secure_cookies) def csp=(new_csp) if self.dynamic_csp - raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= isntead." + raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead." end @csp = new_csp From fc326791105cd8d6364e08fc0245c0c759c2b1e2 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 6 Apr 2016 08:00:22 -0400 Subject: [PATCH 205/636] migrate cookie testing to the SecureHeaders::Cookie tests --- spec/lib/secure_headers/cookie_spec.rb | 54 +++++++++++++--- spec/lib/secure_headers/middleware_spec.rb | 74 ++-------------------- 2 files changed, 52 insertions(+), 76 deletions(-) diff --git a/spec/lib/secure_headers/cookie_spec.rb b/spec/lib/secure_headers/cookie_spec.rb index 51e5c9e0..146f950e 100644 --- a/spec/lib/secure_headers/cookie_spec.rb +++ b/spec/lib/secure_headers/cookie_spec.rb @@ -9,19 +9,55 @@ module SecureHeaders expect(cookie.to_s).to eq(raw_cookie) end - it "flags secure cookies" do - cookie = Cookie.new(raw_cookie, secure: true) - expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) + context "Secure cookies" do + context "when configured with a boolean" do + it "flags cookies as Secure" do + cookie = Cookie.new(raw_cookie, secure: true) + expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) + end + end + + context "when configured with a Hash" do + it "flags cookies as Secure when whitelisted" do + cookie = Cookie.new(raw_cookie, secure: { only: ['_session']}) + expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) + end + + it "does not flag cookies as Secure when excluded" do + cookie = Cookie.new(raw_cookie, secure: { except: ['_session'] }) + expect(cookie.to_s).not_to match(Cookie::SECURE_REGEXP) + end + end end - it "flags HttpOnly cookies" do - cookie = Cookie.new(raw_cookie, httponly: true) - expect(cookie.to_s).to match(Cookie::HTTPONLY_REGEXP) + context "HttpOnly cookies" do + context "when configured with a boolean" do + it "flags cookies as HttpOnly" do + cookie = Cookie.new(raw_cookie, httponly: true) + expect(cookie.to_s).to match(Cookie::HTTPONLY_REGEXP) + end + end + + context "when configured with a Hash" do + it "flags cookies as HttpOnly when whitelisted" do + cookie = Cookie.new(raw_cookie, httponly: { only: ['_session']}) + expect(cookie.to_s).to match(Cookie::HTTPONLY_REGEXP) + end + + it "does not flag cookies as HttpOnly when excluded" do + cookie = Cookie.new(raw_cookie, httponly: { except: ['_session'] }) + expect(cookie.to_s).not_to match(Cookie::HTTPONLY_REGEXP) + end + end end - it "flags SameSite cookies" do - cookie = Cookie.new(raw_cookie, samesite: true) - expect(cookie.to_s).to match(Cookie::SAMESITE_REGEXP) + context "SameSite cookies" do + context "when configured with a boolean" do + it "flags cookies as SameSite" do + cookie = Cookie.new(raw_cookie, samesite: true) + expect(cookie.to_s).to match(Cookie::SAMESITE_REGEXP) + end + end end end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 033e795f..b61349ad 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -63,73 +63,13 @@ module SecureHeaders end context "cookies" do - context "secure cookies" do - context "when secure is a boolean" do - it "flags cookies as secure" do - Configuration.default { |config| config.cookies = { secure: true } } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) - end - end - - context "when secure is a Hash" do - it "flags cookies as secure when whitelisted" do - Configuration.default { |config| config.cookies = { secure: { only: ['foo']} } } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) - end - - it "does not flag cookies as secure when excluded" do - Configuration.default { |config| config.cookies = { secure: { except: ['foo']} } } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) - end - end - end - - context "HttpOnly cookies" do - context "when httponly is a boolean" do - it "flags cookies as HttpOnly" do - Configuration.default { |config| config.cookies = { httponly: true } } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) - end - end - - context "when secure is a Hash" do - it "flags cookies as secure when whitelisted" do - Configuration.default { |config| config.cookies = { httponly: { only: ['foo']} } } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) - end - - it "does not flag cookies as secure when excluded" do - Configuration.default { |config| config.cookies = { httponly: { except: ['foo']} } } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).not_to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) - end - end - end - - context "SameSite cookies" do - context "when samesite is a boolean" do - it "flags cookies as Samesite" do - Configuration.default { |config| config.cookies = { samesite: true } } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SAMESITE_REGEXP) - end - end - - context "when samesite is a Hash" do - skip - end + it "flags cookies from configuration" do + Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: true } } + request = Rack::MockRequest.new(cookie_middleware) + response = request.get '/' + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) + expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SAMESITE_REGEXP) end end end From 1e18eebb6d9e22ce863d7a2cff0ee1c2a7e517d0 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 6 Apr 2016 14:51:31 -0400 Subject: [PATCH 206/636] add SameSite Lax and Strict support --- lib/secure_headers/cookie.rb | 49 ++++++++++++++++++++------ spec/lib/secure_headers/cookie_spec.rb | 40 ++++++++++++++++++--- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/lib/secure_headers/cookie.rb b/lib/secure_headers/cookie.rb index 0446e647..f3d3a499 100644 --- a/lib/secure_headers/cookie.rb +++ b/lib/secure_headers/cookie.rb @@ -5,6 +5,8 @@ class Cookie SECURE_REGEXP = /;\s*secure\s*(;|$)/i.freeze HTTPONLY_REGEXP =/;\s*HttpOnly\s*(;|$)/i.freeze SAMESITE_REGEXP =/;\s*SameSite\s*(;|$)/i.freeze + SAMESITE_LAX_REGEXP =/;\s*SameSite=Lax\s*(;|$)/i.freeze + SAMESITE_STRICT_REGEXP =/;\s*SameSite=Strict\s*(;|$)/i.freeze REGEXES = { secure: SECURE_REGEXP, @@ -23,20 +25,20 @@ def to_s @raw_cookie.dup.tap do |c| c << "; secure" if secure? c << "; HttpOnly" if httponly? - c << "; SameSite" if samesite? + c << "; #{samesite_cookie}" if samesite? end end def secure? - already_flagged?(:secure) || flag_cookie?(:secure) + flag_cookie?(:secure) && !already_flagged?(:secure) end def httponly? - already_flagged?(:httponly) || flag_cookie?(:httponly) + flag_cookie?(:httponly) && !already_flagged?(:httponly) end def samesite? - already_flagged?(:samesite) || flag_samesite? + flag_samesite? && !already_flagged?(:samesite) end private @@ -54,12 +56,31 @@ def flag_cookie?(attribute) when TrueClass true when Hash - if((Array(config[attribute][:only]) & parsed_cookie.keys).any?) - true - elsif((Array(config[attribute][:except]) & parsed_cookie.keys).none?) - true - else - false + conditionally_flag?(config[attribute]) + else + false + end + end + + def conditionally_flag?(configuration) + if((Array(configuration[:only]) & parsed_cookie.keys).any?) + true + elsif((Array(configuration[:except]) & parsed_cookie.keys).none?) + true + else + false + end + end + + def samesite_cookie + case config[:samesite] + when TrueClass + "SameSite" + when Hash + if config[:samesite].key?(:lax) + "SameSite=Lax" + elsif config[:samesite].key?(:strict) + "SameSite=Strict" end else false @@ -70,6 +91,14 @@ def flag_samesite? case config[:samesite] when TrueClass true + when Hash + if config[:samesite].key?(:lax) + conditionally_flag?(config[:samesite][:lax]) + elsif config[:samesite].key?(:strict) + conditionally_flag?(config[:samesite][:strict]) + else + false + end else false end diff --git a/spec/lib/secure_headers/cookie_spec.rb b/spec/lib/secure_headers/cookie_spec.rb index 146f950e..7c663542 100644 --- a/spec/lib/secure_headers/cookie_spec.rb +++ b/spec/lib/secure_headers/cookie_spec.rb @@ -9,6 +9,16 @@ module SecureHeaders expect(cookie.to_s).to eq(raw_cookie) end + it "preserves existing attributes" do + cookie = Cookie.new("_session=thisisatest; secure", secure: true) + expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) + end + + it "prevents duplicate flagging of attributes" do + cookie = Cookie.new("_session=thisisatest; secure", secure: true) + expect(cookie.to_s.scan(/secure/i).count).to eq(1) + end + context "Secure cookies" do context "when configured with a boolean" do it "flags cookies as Secure" do @@ -19,12 +29,12 @@ module SecureHeaders context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do - cookie = Cookie.new(raw_cookie, secure: { only: ['_session']}) + cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}) expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) end it "does not flag cookies as Secure when excluded" do - cookie = Cookie.new(raw_cookie, secure: { except: ['_session'] }) + cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }) expect(cookie.to_s).not_to match(Cookie::SECURE_REGEXP) end end @@ -40,12 +50,12 @@ module SecureHeaders context "when configured with a Hash" do it "flags cookies as HttpOnly when whitelisted" do - cookie = Cookie.new(raw_cookie, httponly: { only: ['_session']}) + cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}) expect(cookie.to_s).to match(Cookie::HTTPONLY_REGEXP) end it "does not flag cookies as HttpOnly when excluded" do - cookie = Cookie.new(raw_cookie, httponly: { except: ['_session'] }) + cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }) expect(cookie.to_s).not_to match(Cookie::HTTPONLY_REGEXP) end end @@ -58,6 +68,28 @@ module SecureHeaders expect(cookie.to_s).to match(Cookie::SAMESITE_REGEXP) end end + + context "when configured with a Hash" do + it "flags SameSite=Lax" do + cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }) + expect(cookie.to_s).to match(Cookie::SAMESITE_LAX_REGEXP) + end + + it "does not flag cookies as SameSite=Lax when excluded" do + cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }) + expect(cookie.to_s).not_to match(Cookie::SAMESITE_LAX_REGEXP) + end + + it "flags SameSite=Strict" do + cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }) + expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) + end + + it "does not flag cookies as SameSite=Strict when excluded" do + cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } }) + expect(cookie.to_s).not_to match(Cookie::SAMESITE_STRICT_REGEXP) + end + end end end end From f1eda56db5e50090422cc4a3155a7b741b2305b1 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 6 Apr 2016 15:40:45 -0400 Subject: [PATCH 207/636] validate cookie configuration --- lib/secure_headers/configuration.rb | 1 + lib/secure_headers/cookie.rb | 29 ++++++++++++++++++++++++++ spec/lib/secure_headers/cookie_spec.rb | 26 +++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index c31671bc..a003de8b 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -164,6 +164,7 @@ def validate_config! XDownloadOptions.validate_config!(@x_download_options) XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies) PublicKeyPins.validate_config!(@hpkp) + Cookie.validate_config!(@cookies) end def secure_cookies=(secure_cookies) diff --git a/lib/secure_headers/cookie.rb b/lib/secure_headers/cookie.rb index f3d3a499..ee61a38a 100644 --- a/lib/secure_headers/cookie.rb +++ b/lib/secure_headers/cookie.rb @@ -1,6 +1,7 @@ require 'cgi' module SecureHeaders + class CookiesConfigError < StandardError; end class Cookie SECURE_REGEXP = /;\s*secure\s*(;|$)/i.freeze HTTPONLY_REGEXP =/;\s*HttpOnly\s*(;|$)/i.freeze @@ -14,6 +15,34 @@ class Cookie samesite: SAMESITE_REGEXP, } + class << self + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise CookiesConfigError.new("config must be a hash.") unless config.is_a? Hash + + # validate only boolean or Hash configuration + [:secure, :httponly, :samesite].each do |attribute| + if config[attribute] && !(config[attribute].is_a?(Hash) || config[attribute].is_a?(TrueClass) || config[attribute].is_a?(FalseClass)) + raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") + end + end + + [:secure, :httponly].each do |attribute| + if config[attribute].is_a?(Hash) && config[attribute].key?(:only) && config[attribute].key?(:except) + raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + end + end + + if config[:samesite] && config[:samesite].is_a?(Hash) + [:lax, :strict].each do |samesite_attribute| + if config[:samesite][samesite_attribute].is_a?(Hash) && config[:samesite][samesite_attribute].key?(:only) && config[:samesite][samesite_attribute].key?(:except) + raise CookiesConfigError.new("samesite #{samesite_attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + end + end + end + end + end + attr_reader :raw_cookie, :config def initialize(cookie, config) diff --git a/spec/lib/secure_headers/cookie_spec.rb b/spec/lib/secure_headers/cookie_spec.rb index 7c663542..a138ce14 100644 --- a/spec/lib/secure_headers/cookie_spec.rb +++ b/spec/lib/secure_headers/cookie_spec.rb @@ -92,4 +92,30 @@ module SecureHeaders end end end + + context "with an invalid configuration" do + it "raises an exception when not configured with a Hash" do + expect do + Cookie.validate_config!("configuration") + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when configured without a boolean/Hash" do + expect do + Cookie.validate_config!(secure: "true") + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when both only and except filters are provided" do + expect do + Cookie.validate_config!(secure: { only: [], except: [] }) + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when both only and except filters are provided to SameSite configurations" do + expect do + Cookie.validate_config!(samesite: { lax: { only: [], except: [] } }) + end.to raise_error(CookiesConfigError) + end + end end From 7c9f0ac267754d2357c4e6a9febd484b44e69238 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 6 Apr 2016 16:38:26 -0400 Subject: [PATCH 208/636] fix SameSite cookies when both Lax and Strict are configured --- lib/secure_headers/cookie.rb | 26 +++++++++++++------------- spec/lib/secure_headers/cookie_spec.rb | 5 +++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/secure_headers/cookie.rb b/lib/secure_headers/cookie.rb index ee61a38a..a624507c 100644 --- a/lib/secure_headers/cookie.rb +++ b/lib/secure_headers/cookie.rb @@ -92,9 +92,9 @@ def flag_cookie?(attribute) end def conditionally_flag?(configuration) - if((Array(configuration[:only]) & parsed_cookie.keys).any?) + if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?) true - elsif((Array(configuration[:except]) & parsed_cookie.keys).none?) + elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?) true else false @@ -106,13 +106,11 @@ def samesite_cookie when TrueClass "SameSite" when Hash - if config[:samesite].key?(:lax) + if flag_samesite_lax? "SameSite=Lax" - elsif config[:samesite].key?(:strict) + elsif flag_samesite_strict? "SameSite=Strict" end - else - false end end @@ -121,16 +119,18 @@ def flag_samesite? when TrueClass true when Hash - if config[:samesite].key?(:lax) - conditionally_flag?(config[:samesite][:lax]) - elsif config[:samesite].key?(:strict) - conditionally_flag?(config[:samesite][:strict]) - else - false - end + flag_samesite_lax? || flag_samesite_strict? else false end end + + def flag_samesite_lax? + config[:samesite].key?(:lax) && conditionally_flag?(config[:samesite][:lax]) + end + + def flag_samesite_strict? + config[:samesite].key?(:strict) && conditionally_flag?(config[:samesite][:strict]) + end end end diff --git a/spec/lib/secure_headers/cookie_spec.rb b/spec/lib/secure_headers/cookie_spec.rb index a138ce14..6c0d9bc1 100644 --- a/spec/lib/secure_headers/cookie_spec.rb +++ b/spec/lib/secure_headers/cookie_spec.rb @@ -89,6 +89,11 @@ module SecureHeaders cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } }) expect(cookie.to_s).not_to match(Cookie::SAMESITE_STRICT_REGEXP) end + + it "flags properly when both lax and strict are configured" do + cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }) + expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) + end end end end From 86d4be69c89b5804f1ed3188dc98993637438fb3 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 6 Apr 2016 16:47:47 -0400 Subject: [PATCH 209/636] relocate cookie class to live alongside headers --- lib/secure_headers.rb | 2 +- lib/secure_headers/{ => headers}/cookie.rb | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/secure_headers/{ => headers}/cookie.rb (100%) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 588b7677..bfdb8455 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -1,5 +1,5 @@ require "secure_headers/configuration" -require "secure_headers/cookie" +require "secure_headers/headers/cookie" require "secure_headers/headers/public_key_pins" require "secure_headers/headers/content_security_policy" require "secure_headers/headers/x_frame_options" diff --git a/lib/secure_headers/cookie.rb b/lib/secure_headers/headers/cookie.rb similarity index 100% rename from lib/secure_headers/cookie.rb rename to lib/secure_headers/headers/cookie.rb From bbabd62c057ab0ebff0cf6ee4d65aa2178ef1a22 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 6 Apr 2016 17:05:30 -0400 Subject: [PATCH 210/636] disable Secure cookies for non-https requests this fixes #243 by disabling the Secure cookies configuration when the request scheme is determined to be anything other than `https` --- lib/secure_headers/middleware.rb | 22 ++++++++++++++- spec/lib/secure_headers/middleware_spec.rb | 31 ++++++++++++++-------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index ecd11f97..84013302 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -10,7 +10,7 @@ def call(env) status, headers, response = @app.call(env) config = SecureHeaders.config_for(req) - flag_cookies!(headers, config.cookies) if config.cookies + flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] end @@ -28,5 +28,25 @@ def flag_cookies!(headers, config) end.join("\n") end end + + # disable Secure cookies for non-https requests + def override_secure(env, config = {}) + if scheme(env) != 'https' + config.merge!(secure: false) + end + + config + end + + # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119 + def scheme(env) + if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on' + 'https' + elsif env['HTTP_X_FORWARDED_PROTO'] + env['HTTP_X_FORWARDED_PROTO'].split(',')[0] + else + env['rack.url_scheme'] + end + end end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index b61349ad..50cb4332 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -44,9 +44,9 @@ module SecureHeaders capture_warning do Configuration.default { |config| config.secure_cookies = true } end - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) + request = Rack::Request.new("HTTPS" => "on") + _, env = cookie_middleware.call request.env + expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) end end @@ -55,9 +55,9 @@ module SecureHeaders capture_warning do Configuration.default { |config| config.secure_cookies = false } end - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) + request = Rack::Request.new("HTTPS" => "on") + _, env = cookie_middleware.call request.env + expect(env['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) end end end @@ -65,11 +65,20 @@ module SecureHeaders context "cookies" do it "flags cookies from configuration" do Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: true } } - request = Rack::MockRequest.new(cookie_middleware) - response = request.get '/' - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) - expect(response.headers['Set-Cookie']).to match(SecureHeaders::Cookie::SAMESITE_REGEXP) + request = Rack::Request.new("HTTPS" => "on") + _, env = cookie_middleware.call request.env + + expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) + expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) + expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SAMESITE_REGEXP) + end + + it "disables secure cookies for non-https requests" do + Configuration.default { |config| config.cookies = { secure: true } } + + request = Rack::Request.new("HTTPS" => "off") + _, env = cookie_middleware.call request.env + expect(env['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) end end end From 02b13250403faf6d98c02f8e5f049d6303477ee7 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 6 Apr 2016 17:27:06 -0400 Subject: [PATCH 211/636] document cookie configuration --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 582123e2..586f4bc8 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,11 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` ```ruby SecureHeaders::Configuration.default do |config| - config.secure_cookies = true # mark all cookies as "secure" + config.cookies = { + secure: true, # mark all cookies as "secure" + httponly: true, # mark all cookies as "httponly" + samesite: true + } config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" @@ -264,6 +268,55 @@ config.hpkp = { } ``` +### Cookies + +SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-06) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. + +__Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. + +#### Boolean-based configuration + +Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. + +```ruby +config.cookies = { + secure: true, # mark all cookies as Secure + httponly: false, # do not mark any cookies as HttpOnly +} +``` + +#### Hash-based configuration + +Hash-based configuration allows for fine-grained control. + +```ruby +config.cookies = { + secure: { except: ['_guest'], # mark all but the `_guest` cookie as Secure + httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly +} +``` + +#### SameSite + +SameSite cookies permit either `Strict` or `Lax` enforcement mode options. + +```ruby +config.cookies = { + samesite: true # mark all cookies as SameSite (user agents default this to `Strict` enforcement mode) +} +``` + +`Strict` and `Lax` enforcement can also be specified using a Hash. + +```ruby +config.cookies = { + samesite: { + strict: { only: [`_rails_session`] }, + lax: { only: [`_guest`] } + } +} +``` + ### Using with Sinatra Here's an example using SecureHeaders for Sinatra applications: From f8bb96571b4afef9ae075452395f4d21f5779b68 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 6 Apr 2016 17:28:11 -0400 Subject: [PATCH 212/636] small documentation tweak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 586f4bc8..2290ef85 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ config.cookies = { } ``` -#### SameSite +#### SameSite cookies SameSite cookies permit either `Strict` or `Lax` enforcement mode options. From 86de836cad777464d6a5f789fbb057211a699dfc Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 7 Apr 2016 11:17:41 -0400 Subject: [PATCH 213/636] add closing parentheses to example in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2290ef85..14352a35 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ Hash-based configuration allows for fine-grained control. ```ruby config.cookies = { - secure: { except: ['_guest'], # mark all but the `_guest` cookie as Secure + secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly } ``` From 1a0a4fc14856b7d4d65b0332abbf9d90694b3923 Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 7 Apr 2016 11:20:35 -0400 Subject: [PATCH 214/636] update link to latest version of SameSite draft spec --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14352a35..641b22f1 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ config.hpkp = { ### Cookies -SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-06) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. +SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. From 6dda46ab98583923589318b461bf08dce7e27182 Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 7 Apr 2016 12:41:21 -0400 Subject: [PATCH 215/636] add combination test of Strict and Lax SameSite cookies --- spec/lib/secure_headers/middleware_spec.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 50cb4332..6acb8da0 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -73,6 +73,17 @@ module SecureHeaders expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SAMESITE_REGEXP) end + it "flags cookies with a combination of SameSite configurations" do + cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] }) + + Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } } } } + request = Rack::Request.new("HTTPS" => "on") + _, env = cookie_middleware.call request.env + + expect(env['Set-Cookie']).to match("_session=foobar; SameSite=Strict") + expect(env['Set-Cookie']).to match("_guest=true; SameSite=Lax") + end + it "disables secure cookies for non-https requests" do Configuration.default { |config| config.cookies = { secure: true } } From 72ae1f0977dbbfbfd57ad08708f559402b6929d2 Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 7 Apr 2016 21:49:50 -0400 Subject: [PATCH 216/636] update SameSite cookie configuration global boolean config is not permitted, `lax` and `strict` can accept booleans to enable for all cookies --- lib/secure_headers/headers/cookie.rb | 91 +++++++++++++++------- spec/lib/secure_headers/cookie_spec.rb | 86 +++++++++++++------- spec/lib/secure_headers/middleware_spec.rb | 3 +- 3 files changed, 124 insertions(+), 56 deletions(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index a624507c..850b8f9a 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -20,23 +20,59 @@ def validate_config!(config) return if config.nil? || config == OPT_OUT raise CookiesConfigError.new("config must be a hash.") unless config.is_a? Hash - # validate only boolean or Hash configuration - [:secure, :httponly, :samesite].each do |attribute| + # secure and httponly - validate only boolean or Hash configuration + [:secure, :httponly].each do |attribute| if config[attribute] && !(config[attribute].is_a?(Hash) || config[attribute].is_a?(TrueClass) || config[attribute].is_a?(FalseClass)) raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") end end + # secure and httponly - validate exclusive use of only or except but not both at the same time [:secure, :httponly].each do |attribute| - if config[attribute].is_a?(Hash) && config[attribute].key?(:only) && config[attribute].key?(:except) - raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + if config[attribute].is_a?(Hash) + if config[attribute].key?(:only) && config[attribute].key?(:except) + raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + end + + if (intersection = (config[attribute].fetch(:only, []) & config[attribute].fetch(:only, []))).any? + raise CookiesConfigError.new("#{attribute} cookie config is invalid, cookies #{intersection.join(', ')} cannot be enforced as lax and strict") + end end end - if config[:samesite] && config[:samesite].is_a?(Hash) - [:lax, :strict].each do |samesite_attribute| - if config[:samesite][samesite_attribute].is_a?(Hash) && config[:samesite][samesite_attribute].key?(:only) && config[:samesite][samesite_attribute].key?(:except) - raise CookiesConfigError.new("samesite #{samesite_attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + if config[:samesite] + raise CookiesConfigError.new("samesite cookie config must be a hash") unless config[:samesite].is_a?(Hash) + + # when configuring with booleans, only one enforcement is permitted + if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") + elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") + end + + # validate Hash-based samesite configuration + if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(Hash) + # validate exclusive use of only or except but not both at the same time + if config[:samesite][:lax].key?(:only) && config[:samesite][:lax].key?(:except) + raise CookiesConfigError.new("samesite lax cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + end + + if config[:samesite].key?(:strict) + # validate exclusivity of only and except members + if (intersection = (config[:samesite][:lax].fetch(:only, []) & config[:samesite][:strict].fetch(:only, []))).any? + raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") + end + + if (intersection = (config[:samesite][:lax].fetch(:except, []) & config[:samesite][:strict].fetch(:except, []))).any? + raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") + end + end + end + + if config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(Hash) + # validate exclusive use of only or except but not both at the same time + if config[:samesite][:strict].key?(:only) && config[:samesite][:strict].key?(:except) + raise CookiesConfigError.new("samesite strict cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") end end end @@ -102,35 +138,36 @@ def conditionally_flag?(configuration) end def samesite_cookie - case config[:samesite] - when TrueClass - "SameSite" - when Hash - if flag_samesite_lax? - "SameSite=Lax" - elsif flag_samesite_strict? - "SameSite=Strict" - end + if flag_samesite_lax? + "SameSite=Lax" + elsif flag_samesite_strict? + "SameSite=Strict" end end def flag_samesite? - case config[:samesite] - when TrueClass - true - when Hash - flag_samesite_lax? || flag_samesite_strict? - else - false - end + flag_samesite_lax? || flag_samesite_strict? end def flag_samesite_lax? - config[:samesite].key?(:lax) && conditionally_flag?(config[:samesite][:lax]) + flag_samesite_enforcement?(:lax) end def flag_samesite_strict? - config[:samesite].key?(:strict) && conditionally_flag?(config[:samesite][:strict]) + flag_samesite_enforcement?(:strict) + end + + def flag_samesite_enforcement?(mode) + return unless config[:samesite] + + case config[:samesite][mode] + when Hash + conditionally_flag?(config[:samesite][mode]) + when TrueClass + true + else + false + end end end end diff --git a/spec/lib/secure_headers/cookie_spec.rb b/spec/lib/secure_headers/cookie_spec.rb index 6c0d9bc1..9cbeb624 100644 --- a/spec/lib/secure_headers/cookie_spec.rb +++ b/spec/lib/secure_headers/cookie_spec.rb @@ -62,38 +62,40 @@ module SecureHeaders end context "SameSite cookies" do - context "when configured with a boolean" do - it "flags cookies as SameSite" do - cookie = Cookie.new(raw_cookie, samesite: true) - expect(cookie.to_s).to match(Cookie::SAMESITE_REGEXP) - end + it "flags SameSite=Lax" do + cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }) + expect(cookie.to_s).to match(Cookie::SAMESITE_LAX_REGEXP) end - context "when configured with a Hash" do - it "flags SameSite=Lax" do - cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }) - expect(cookie.to_s).to match(Cookie::SAMESITE_LAX_REGEXP) - end + it "flags SameSite=Lax when configured with a boolean" do + cookie = Cookie.new(raw_cookie, samesite: { lax: true}) + expect(cookie.to_s).to match(Cookie::SAMESITE_LAX_REGEXP) + end - it "does not flag cookies as SameSite=Lax when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }) - expect(cookie.to_s).not_to match(Cookie::SAMESITE_LAX_REGEXP) - end + it "does not flag cookies as SameSite=Lax when excluded" do + cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }) + expect(cookie.to_s).not_to match(Cookie::SAMESITE_LAX_REGEXP) + end - it "flags SameSite=Strict" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }) - expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) - end + it "flags SameSite=Strict" do + cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }) + expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) + end - it "does not flag cookies as SameSite=Strict when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } }) - expect(cookie.to_s).not_to match(Cookie::SAMESITE_STRICT_REGEXP) - end + it "does not flag cookies as SameSite=Strict when excluded" do + cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } }) + expect(cookie.to_s).not_to match(Cookie::SAMESITE_STRICT_REGEXP) + end - it "flags properly when both lax and strict are configured" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }) - expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) - end + it "flags SameSite=Strict when configured with a boolean" do + cookie = Cookie.new(raw_cookie, samesite: { strict: true}) + expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) + end + + it "flags properly when both lax and strict are configured" do + raw_cookie = "_session=thisisatest" + cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }) + expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) end end end @@ -117,9 +119,39 @@ module SecureHeaders end.to raise_error(CookiesConfigError) end + it "raises an exception when SameSite is not configured with a Hash" do + expect do + Cookie.validate_config!(samesite: true) + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do + expect do + Cookie.validate_config!(samesite: { lax: true, strict: true}) + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do + expect do + Cookie.validate_config!(samesite: { lax: true, strict: { only: ["_anything"] } }) + end.to raise_error(CookiesConfigError) + end + it "raises an exception when both only and except filters are provided to SameSite configurations" do expect do - Cookie.validate_config!(samesite: { lax: { only: [], except: [] } }) + Cookie.validate_config!(samesite: { lax: { only: ["_anything"], except: ["_anythingelse"] } }) + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do + expect do + Cookie.validate_config!(samesite: { lax: { only: ["_anything"] }, strict: { only: ["_anything"] } }) + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do + expect do + Cookie.validate_config!(samesite: { lax: { except: ["_anything"] }, strict: { except: ["_anything"] } }) end.to raise_error(CookiesConfigError) end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 6acb8da0..d4f62b3e 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -64,13 +64,12 @@ module SecureHeaders context "cookies" do it "flags cookies from configuration" do - Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: true } } + Configuration.default { |config| config.cookies = { secure: true, httponly: true } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) - expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SAMESITE_REGEXP) end it "flags cookies with a combination of SameSite configurations" do From 754055bcbbc99ac8002ea5c207e50d792b1d8ac1 Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 7 Apr 2016 21:53:37 -0400 Subject: [PATCH 217/636] update documentation for SameSite cookie configurations --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 641b22f1..96e98a2d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The gem will automatically apply several headers that are related to security. - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) -It can also mark all http cookies with the secure attribute (when configured to do so). +It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so). `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings. @@ -296,17 +296,19 @@ config.cookies = { } ``` -#### SameSite cookies +#### SameSite cookie configuration SameSite cookies permit either `Strict` or `Lax` enforcement mode options. ```ruby config.cookies = { - samesite: true # mark all cookies as SameSite (user agents default this to `Strict` enforcement mode) + samesite: { + strict: true # mark all cookies as SameSite=Strict + } } ``` -`Strict` and `Lax` enforcement can also be specified using a Hash. +`Strict` and `Lax` enforcement modes can also be specified using a Hash. ```ruby config.cookies = { From 2e74b8120a75144e952afe06ebe7ad12d1c98e5f Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 7 Apr 2016 22:21:42 -0400 Subject: [PATCH 218/636] parse the cookie to determine existing flags --- lib/secure_headers/headers/cookie.rb | 28 +++++++++++++++++++------- spec/lib/secure_headers/cookie_spec.rb | 6 ++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 850b8f9a..ef690966 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -9,12 +9,6 @@ class Cookie SAMESITE_LAX_REGEXP =/;\s*SameSite=Lax\s*(;|$)/i.freeze SAMESITE_STRICT_REGEXP =/;\s*SameSite=Strict\s*(;|$)/i.freeze - REGEXES = { - secure: SECURE_REGEXP, - httponly: HTTPONLY_REGEXP, - samesite: SAMESITE_REGEXP, - } - class << self def validate_config!(config) return if config.nil? || config == OPT_OUT @@ -84,6 +78,13 @@ def validate_config!(config) def initialize(cookie, config) @raw_cookie = cookie @config = config + @attributes = { + "secure" => nil, + "httponly" => nil, + "samesite" => nil, + } + + parse(cookie) end def to_s @@ -113,7 +114,7 @@ def parsed_cookie end def already_flagged?(attribute) - raw_cookie =~ REGEXES[attribute] + @attributes[attribute.to_s] end def flag_cookie?(attribute) @@ -169,5 +170,18 @@ def flag_samesite_enforcement?(mode) false end end + + def parse(cookie) + return unless cookie + + cookie.split(/[;,]\s?/).each do |pairs| + name, values = pairs.split('=',2) + name = CGI.unescape(name) + + if @attributes.has_key?(name.downcase) + @attributes[name.downcase] = values || true + end + end + end end end diff --git a/spec/lib/secure_headers/cookie_spec.rb b/spec/lib/secure_headers/cookie_spec.rb index 9cbeb624..9addd8f1 100644 --- a/spec/lib/secure_headers/cookie_spec.rb +++ b/spec/lib/secure_headers/cookie_spec.rb @@ -97,6 +97,12 @@ module SecureHeaders cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }) expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) end + + it "ignores configuration if the cookie is already flagged" do + raw_cookie = "_session=thisisatest; SameSite=Strict" + cookie = Cookie.new(raw_cookie, samesite: { lax: true }) + expect(cookie.to_s).to eq(raw_cookie) + end end end From 66c1b4ed8fed2545a13b9d029e231ed8bf6768a7 Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 7 Apr 2016 22:22:17 -0400 Subject: [PATCH 219/636] move cookie spec to headers directory --- spec/lib/secure_headers/{ => headers}/cookie_spec.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/lib/secure_headers/{ => headers}/cookie_spec.rb (100%) diff --git a/spec/lib/secure_headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb similarity index 100% rename from spec/lib/secure_headers/cookie_spec.rb rename to spec/lib/secure_headers/headers/cookie_spec.rb From e69dc4d15b6ed3b9b80442be0b6275e86b100b6e Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 13 Apr 2016 09:17:43 -0400 Subject: [PATCH 220/636] correct readme example --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 96e98a2d..2683bcda 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,11 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` ```ruby SecureHeaders::Configuration.default do |config| config.cookies = { - secure: true, # mark all cookies as "secure" - httponly: true, # mark all cookies as "httponly" - samesite: true + secure: true, # mark all cookies as "Secure" + httponly: true, # mark all cookies as "HttpOnly" + samesite: { + strict: true # mark all cookies as SameSite=Strict + } } config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" config.x_frame_options = "DENY" From b291397989dfe7a8a4f16cfbbe764cc6e566a19d Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 13 Apr 2016 11:21:23 -0400 Subject: [PATCH 221/636] store parsed cookie attributes as symbols --- lib/secure_headers/headers/cookie.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index ef690966..2ccf73b7 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -79,9 +79,9 @@ def initialize(cookie, config) @raw_cookie = cookie @config = config @attributes = { - "secure" => nil, - "httponly" => nil, - "samesite" => nil, + httponly: nil, + samesite: nil, + secure: nil, } parse(cookie) @@ -114,7 +114,7 @@ def parsed_cookie end def already_flagged?(attribute) - @attributes[attribute.to_s] + @attributes[attribute] end def flag_cookie?(attribute) @@ -178,8 +178,9 @@ def parse(cookie) name, values = pairs.split('=',2) name = CGI.unescape(name) - if @attributes.has_key?(name.downcase) - @attributes[name.downcase] = values || true + attribute = name.downcase.to_sym + if @attributes.has_key?(attribute) + @attributes[attribute] = values || true end end end From f11c7e5c7c264a5312e1185bc86828d3b16b6a9f Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 13 Apr 2016 11:29:01 -0400 Subject: [PATCH 222/636] switch from regex matchers to string equality matchers --- lib/secure_headers/headers/cookie.rb | 5 ---- .../lib/secure_headers/headers/cookie_spec.rb | 28 +++++++++---------- spec/lib/secure_headers/middleware_spec.rb | 9 +++--- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 2ccf73b7..12d4c2e1 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -3,11 +3,6 @@ module SecureHeaders class CookiesConfigError < StandardError; end class Cookie - SECURE_REGEXP = /;\s*secure\s*(;|$)/i.freeze - HTTPONLY_REGEXP =/;\s*HttpOnly\s*(;|$)/i.freeze - SAMESITE_REGEXP =/;\s*SameSite\s*(;|$)/i.freeze - SAMESITE_LAX_REGEXP =/;\s*SameSite=Lax\s*(;|$)/i.freeze - SAMESITE_STRICT_REGEXP =/;\s*SameSite=Strict\s*(;|$)/i.freeze class << self def validate_config!(config) diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index 9addd8f1..15f28076 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -11,7 +11,7 @@ module SecureHeaders it "preserves existing attributes" do cookie = Cookie.new("_session=thisisatest; secure", secure: true) - expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "prevents duplicate flagging of attributes" do @@ -23,19 +23,19 @@ module SecureHeaders context "when configured with a boolean" do it "flags cookies as Secure" do cookie = Cookie.new(raw_cookie, secure: true) - expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; secure") end end context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}) - expect(cookie.to_s).to match(Cookie::SECURE_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "does not flag cookies as Secure when excluded" do cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }) - expect(cookie.to_s).not_to match(Cookie::SECURE_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest") end end end @@ -44,19 +44,19 @@ module SecureHeaders context "when configured with a boolean" do it "flags cookies as HttpOnly" do cookie = Cookie.new(raw_cookie, httponly: true) - expect(cookie.to_s).to match(Cookie::HTTPONLY_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end end context "when configured with a Hash" do it "flags cookies as HttpOnly when whitelisted" do cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}) - expect(cookie.to_s).to match(Cookie::HTTPONLY_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end it "does not flag cookies as HttpOnly when excluded" do cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }) - expect(cookie.to_s).not_to match(Cookie::HTTPONLY_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest") end end end @@ -64,38 +64,38 @@ module SecureHeaders context "SameSite cookies" do it "flags SameSite=Lax" do cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }) - expect(cookie.to_s).to match(Cookie::SAMESITE_LAX_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end it "flags SameSite=Lax when configured with a boolean" do cookie = Cookie.new(raw_cookie, samesite: { lax: true}) - expect(cookie.to_s).to match(Cookie::SAMESITE_LAX_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end it "does not flag cookies as SameSite=Lax when excluded" do cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }) - expect(cookie.to_s).not_to match(Cookie::SAMESITE_LAX_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest") end it "flags SameSite=Strict" do cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }) - expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "does not flag cookies as SameSite=Strict when excluded" do cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } }) - expect(cookie.to_s).not_to match(Cookie::SAMESITE_STRICT_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest") end it "flags SameSite=Strict when configured with a boolean" do cookie = Cookie.new(raw_cookie, samesite: { strict: true}) - expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "flags properly when both lax and strict are configured" do raw_cookie = "_session=thisisatest" cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }) - expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "ignores configuration if the cookie is already flagged" do diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index d4f62b3e..efa853eb 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -46,7 +46,7 @@ module SecureHeaders end request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) + expect(env['Set-Cookie']).to eq("foo=bar; secure") end end @@ -57,7 +57,7 @@ module SecureHeaders end request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) + expect(env['Set-Cookie']).to eq("foo=bar") end end end @@ -68,8 +68,7 @@ module SecureHeaders request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP) - expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP) + expect(env['Set-Cookie']).to eq("foo=bar; secure; HttpOnly") end it "flags cookies with a combination of SameSite configurations" do @@ -88,7 +87,7 @@ module SecureHeaders request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).not_to match(SecureHeaders::Cookie::SECURE_REGEXP) + expect(env['Set-Cookie']).to eq("foo=bar") end end end From 075e4950fb4616caaa54c64ff562c229be7f42bf Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 13 Apr 2016 18:58:57 -0400 Subject: [PATCH 223/636] fix README example syntax --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2683bcda..06866d4b 100644 --- a/README.md +++ b/README.md @@ -315,8 +315,8 @@ config.cookies = { ```ruby config.cookies = { samesite: { - strict: { only: [`_rails_session`] }, - lax: { only: [`_guest`] } + strict: { only: ['_rails_session'] }, + lax: { only: ['_guest'] } } } ``` From 9a8c5a1774e2c6c96abec30ded66fa47fd800b9f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 13 Apr 2016 14:35:14 -1000 Subject: [PATCH 224/636] make location of hash file configurable via ENV --- lib/secure_headers/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index f92d3354..d9bfd00d 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -108,7 +108,7 @@ def deep_copy_if_hash(value) attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies - HASH_CONFIG_FILE = 'config/secure_headers_generated_hashes.yml' + HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml" if File.exists?(HASH_CONFIG_FILE) config = YAML.safe_load(File.open(HASH_CONFIG_FILE)) @script_hashes = config["scripts"] From bb2715deff470c62a270fd13946a9fd8260fac55 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 13 Apr 2016 14:49:28 -1000 Subject: [PATCH 225/636] add documentation around hashed content --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 582123e2..7437e04f 100644 --- a/README.md +++ b/README.md @@ -240,9 +240,69 @@ body { ``` +``` +Content-Security-Policy: ... + script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; + style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; +``` + #### Hash -The hash feature has been removed, for now. +`script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. + +You can add hash sources directly to your policy : + +```ruby +::SecureHeaders::Configuration.default do |config| + config.csp = { + default_src: %w('self') + + # this is a made up value but browsers will show the expected hash in the console. + script_src: %w(sha256-123456) + } + end + ``` + + You can also use the automated inline script detection/collection/computation of hash source values in your app. + + ```bash + rake secure_headers:generate_hashes + ``` + + This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. + +```yaml +--- +scripts: + app/views/asdfs/index.html.erb: + - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='" +styles: + app/views/asdfs/index.html.erb: + - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" + - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" +``` + +``` +Content-Security-Policy: ... + script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; + style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; +``` + +##### Helpers + +You can use a helper that will raise exceptions when it encounters unknown hash values. Note: this is pretty limited as it will only raise if called on a file with no associated hashes. It will not raise an error if an unknown script is added to a file with known hashses. Also, it won't compute dynamic hashes by design. The output of both helpers will be a plain `script`/`style` tag without modification. + +```erb +<%= hashed_javascript_tag do %> +console.log("hai"); +<% end %> + +<%= hashed_style_tag do %> +body { + background-color: black; +} +<% end %> +``` ### Public Key Pins From fa56adce8d0eb3ca5c52c1944caab410981b3178 Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 13 Apr 2016 23:50:41 -0400 Subject: [PATCH 226/636] extract cookies configuration validation to separate class --- lib/secure_headers/headers/cookie.rb | 61 +----------------- lib/secure_headers/utils/cookies_config.rb | 72 ++++++++++++++++++++++ 2 files changed, 74 insertions(+), 59 deletions(-) create mode 100644 lib/secure_headers/utils/cookies_config.rb diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 12d4c2e1..c4bd0d41 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -1,4 +1,5 @@ require 'cgi' +require 'secure_headers/utils/cookies_config' module SecureHeaders class CookiesConfigError < StandardError; end @@ -6,65 +7,7 @@ class Cookie class << self def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise CookiesConfigError.new("config must be a hash.") unless config.is_a? Hash - - # secure and httponly - validate only boolean or Hash configuration - [:secure, :httponly].each do |attribute| - if config[attribute] && !(config[attribute].is_a?(Hash) || config[attribute].is_a?(TrueClass) || config[attribute].is_a?(FalseClass)) - raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") - end - end - - # secure and httponly - validate exclusive use of only or except but not both at the same time - [:secure, :httponly].each do |attribute| - if config[attribute].is_a?(Hash) - if config[attribute].key?(:only) && config[attribute].key?(:except) - raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") - end - - if (intersection = (config[attribute].fetch(:only, []) & config[attribute].fetch(:only, []))).any? - raise CookiesConfigError.new("#{attribute} cookie config is invalid, cookies #{intersection.join(', ')} cannot be enforced as lax and strict") - end - end - end - - if config[:samesite] - raise CookiesConfigError.new("samesite cookie config must be a hash") unless config[:samesite].is_a?(Hash) - - # when configuring with booleans, only one enforcement is permitted - if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict) - raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") - elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax) - raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") - end - - # validate Hash-based samesite configuration - if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(Hash) - # validate exclusive use of only or except but not both at the same time - if config[:samesite][:lax].key?(:only) && config[:samesite][:lax].key?(:except) - raise CookiesConfigError.new("samesite lax cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") - end - - if config[:samesite].key?(:strict) - # validate exclusivity of only and except members - if (intersection = (config[:samesite][:lax].fetch(:only, []) & config[:samesite][:strict].fetch(:only, []))).any? - raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") - end - - if (intersection = (config[:samesite][:lax].fetch(:except, []) & config[:samesite][:strict].fetch(:except, []))).any? - raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") - end - end - end - - if config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(Hash) - # validate exclusive use of only or except but not both at the same time - if config[:samesite][:strict].key?(:only) && config[:samesite][:strict].key?(:except) - raise CookiesConfigError.new("samesite strict cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") - end - end - end + CookiesConfig.new(config).valid? end end diff --git a/lib/secure_headers/utils/cookies_config.rb b/lib/secure_headers/utils/cookies_config.rb new file mode 100644 index 00000000..f85cb5e7 --- /dev/null +++ b/lib/secure_headers/utils/cookies_config.rb @@ -0,0 +1,72 @@ +module SecureHeaders + class CookiesConfig + + attr_reader :config + + def initialize(config) + @config = config + end + + def valid? + return if config.nil? || config == OPT_OUT + raise CookiesConfigError.new("config must be a hash.") unless config.is_a? Hash + + # secure and httponly - validate only boolean or Hash configuration + [:secure, :httponly].each do |attribute| + if config[attribute] && !(config[attribute].is_a?(Hash) || config[attribute].is_a?(TrueClass) || config[attribute].is_a?(FalseClass)) + raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") + end + end + + # secure and httponly - validate exclusive use of only or except but not both at the same time + [:secure, :httponly].each do |attribute| + if config[attribute].is_a?(Hash) + if config[attribute].key?(:only) && config[attribute].key?(:except) + raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + end + + if (intersection = (config[attribute].fetch(:only, []) & config[attribute].fetch(:only, []))).any? + raise CookiesConfigError.new("#{attribute} cookie config is invalid, cookies #{intersection.join(', ')} cannot be enforced as lax and strict") + end + end + end + + if config[:samesite] + raise CookiesConfigError.new("samesite cookie config must be a hash") unless config[:samesite].is_a?(Hash) + + # when configuring with booleans, only one enforcement is permitted + if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") + elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") + end + + # validate Hash-based samesite configuration + if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(Hash) + # validate exclusive use of only or except but not both at the same time + if config[:samesite][:lax].key?(:only) && config[:samesite][:lax].key?(:except) + raise CookiesConfigError.new("samesite lax cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + end + + if config[:samesite].key?(:strict) + # validate exclusivity of only and except members + if (intersection = (config[:samesite][:lax].fetch(:only, []) & config[:samesite][:strict].fetch(:only, []))).any? + raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") + end + + if (intersection = (config[:samesite][:lax].fetch(:except, []) & config[:samesite][:strict].fetch(:except, []))).any? + raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") + end + end + end + + if config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(Hash) + # validate exclusive use of only or except but not both at the same time + if config[:samesite][:strict].key?(:only) && config[:samesite][:strict].key?(:except) + raise CookiesConfigError.new("samesite strict cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + end + end + end + end + end +end From bc2bc7593a6fa4390362e433cc1ea8c819082d03 Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 14 Apr 2016 00:45:48 -0400 Subject: [PATCH 227/636] break up cookie validations into separate methods --- lib/secure_headers/headers/cookie.rb | 2 +- lib/secure_headers/utils/cookies_config.rb | 118 ++++++++++++--------- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index c4bd0d41..b3423395 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -7,7 +7,7 @@ class Cookie class << self def validate_config!(config) - CookiesConfig.new(config).valid? + CookiesConfig.new(config).validate! end end diff --git a/lib/secure_headers/utils/cookies_config.rb b/lib/secure_headers/utils/cookies_config.rb index f85cb5e7..cabb354e 100644 --- a/lib/secure_headers/utils/cookies_config.rb +++ b/lib/secure_headers/utils/cookies_config.rb @@ -7,66 +7,88 @@ def initialize(config) @config = config end - def valid? - return if config.nil? || config == OPT_OUT - raise CookiesConfigError.new("config must be a hash.") unless config.is_a? Hash - - # secure and httponly - validate only boolean or Hash configuration - [:secure, :httponly].each do |attribute| - if config[attribute] && !(config[attribute].is_a?(Hash) || config[attribute].is_a?(TrueClass) || config[attribute].is_a?(FalseClass)) - raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") - end - end + def validate! + return if config.nil? || config == SecureHeaders::OPT_OUT - # secure and httponly - validate exclusive use of only or except but not both at the same time - [:secure, :httponly].each do |attribute| - if config[attribute].is_a?(Hash) - if config[attribute].key?(:only) && config[attribute].key?(:except) - raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") - end + validate_config! + validate_secure_config! if config[:secure] + validate_httponly_config! if config[:httponly] + validate_samesite_config! if config[:samesite] + end - if (intersection = (config[attribute].fetch(:only, []) & config[attribute].fetch(:only, []))).any? - raise CookiesConfigError.new("#{attribute} cookie config is invalid, cookies #{intersection.join(', ')} cannot be enforced as lax and strict") - end - end - end + private - if config[:samesite] - raise CookiesConfigError.new("samesite cookie config must be a hash") unless config[:samesite].is_a?(Hash) + def validate_config! + raise CookiesConfigError.new("config must be a hash.") unless is_hash?(config) + end - # when configuring with booleans, only one enforcement is permitted - if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict) - raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") - elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax) - raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") - end + def validate_secure_config! + validate_hash_or_boolean!(:secure) + validate_exclusive_use_of_hash_constraints!(config[:secure], :secure) + end - # validate Hash-based samesite configuration - if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(Hash) - # validate exclusive use of only or except but not both at the same time - if config[:samesite][:lax].key?(:only) && config[:samesite][:lax].key?(:except) - raise CookiesConfigError.new("samesite lax cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") - end + def validate_httponly_config! + validate_hash_or_boolean!(:httponly) + validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly) + end + + def validate_samesite_config! + raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite]) + + validate_samesite_boolean_config! + validate_samesite_hash_config! + end - if config[:samesite].key?(:strict) - # validate exclusivity of only and except members - if (intersection = (config[:samesite][:lax].fetch(:only, []) & config[:samesite][:strict].fetch(:only, []))).any? - raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") - end + # when configuring with booleans, only one enforcement is permitted + def validate_samesite_boolean_config! + if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") + elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") + end + end - if (intersection = (config[:samesite][:lax].fetch(:except, []) & config[:samesite][:strict].fetch(:except, []))).any? - raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") - end + def validate_samesite_hash_config! + # validate Hash-based samesite configuration + if is_hash?(config[:samesite][:lax]) + validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], 'samesite lax') + + if is_hash?(config[:samesite][:strict]) + validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict') + + # validate exclusivity of only and except members within strict and lax + if (intersection = (config[:samesite][:lax].fetch(:only, []) & config[:samesite][:strict].fetch(:only, []))).any? + raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") end - end - if config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(Hash) - # validate exclusive use of only or except but not both at the same time - if config[:samesite][:strict].key?(:only) && config[:samesite][:strict].key?(:except) - raise CookiesConfigError.new("samesite strict cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + if (intersection = (config[:samesite][:lax].fetch(:except, []) & config[:samesite][:strict].fetch(:except, []))).any? + raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") end end end end + + def validate_hash_or_boolean!(attribute) + if !(is_hash?(config[attribute]) || is_boolean?(config[attribute])) + raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") + end + end + + # validate exclusive use of only or except but not both at the same time + def validate_exclusive_use_of_hash_constraints!(conf, attribute) + return unless is_hash?(conf) + + if conf.key?(:only) && conf.key?(:except) + raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") + end + end + + def is_hash?(obj) + obj && obj.is_a?(Hash) + end + + def is_boolean?(obj) + obj && (obj.is_a?(TrueClass) || obj.is_a?(FalseClass)) + end end end From fc8a80b6aa66366d0529076d5ecc6ade3d6b1155 Mon Sep 17 00:00:00 2001 From: stve Date: Thu, 14 Apr 2016 13:47:05 -0400 Subject: [PATCH 228/636] DRY up the samesite only/except usage validations --- lib/secure_headers/utils/cookies_config.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/secure_headers/utils/cookies_config.rb b/lib/secure_headers/utils/cookies_config.rb index cabb354e..ebf89ac2 100644 --- a/lib/secure_headers/utils/cookies_config.rb +++ b/lib/secure_headers/utils/cookies_config.rb @@ -55,15 +55,8 @@ def validate_samesite_hash_config! if is_hash?(config[:samesite][:strict]) validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict') - - # validate exclusivity of only and except members within strict and lax - if (intersection = (config[:samesite][:lax].fetch(:only, []) & config[:samesite][:strict].fetch(:only, []))).any? - raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") - end - - if (intersection = (config[:samesite][:lax].fetch(:except, []) & config[:samesite][:strict].fetch(:except, []))).any? - raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") - end + validate_exclusive_use_of_samesite_enforcement!(:only) + validate_exclusive_use_of_samesite_enforcement!(:except) end end end @@ -83,6 +76,13 @@ def validate_exclusive_use_of_hash_constraints!(conf, attribute) end end + # validate exclusivity of only and except members within strict and lax + def validate_exclusive_use_of_samesite_enforcement!(attribute) + if (intersection = (config[:samesite][:lax].fetch(attribute, []) & config[:samesite][:strict].fetch(attribute, []))).any? + raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") + end + end + def is_hash?(obj) obj && obj.is_a?(Hash) end From 99eed49b72d8aff4c4cae20f3738840457b094b0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 14 Apr 2016 10:15:38 -1000 Subject: [PATCH 229/636] clarify, document, and test behavior when encountering unknown hashes --- README.md | 28 +++++++++++++------- lib/secure_headers/view_helper.rb | 18 +++++-------- spec/lib/secure_headers/view_helpers_spec.rb | 24 +++++++++++------ 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a439a680..d987cb9c 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ body { ``` ``` + Content-Security-Policy: ... script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; @@ -286,26 +287,33 @@ styles: - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" ``` -``` -Content-Security-Policy: ... - script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; - style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; -``` - ##### Helpers -You can use a helper that will raise exceptions when it encounters unknown hash values. Note: this is pretty limited as it will only raise if called on a file with no associated hashes. It will not raise an error if an unknown script is added to a file with known hashses. Also, it won't compute dynamic hashes by design. The output of both helpers will be a plain `script`/`style` tag without modification. +**This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. ```erb -<%= hashed_javascript_tag do %> -console.log("hai"); +<%= hashed_style_tag do %> +body { + background-color: black; +} <% end %> <%= hashed_style_tag do %> body { - background-color: black; + font-size: 30px; + font-color: green; } <% end %> + +<%= hashed_javascript_tag do %> +console.log(1) +<% end %> +``` + +``` +Content-Security-Policy: ... + script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; + style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; ``` ### Public Key Pins diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 70088d05..de5141c8 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -38,8 +38,7 @@ def content_security_policy_nonce(type) # Checks to see if the hashed code is expected and adds the hash source # value to the current CSP. # - # In development/test/etc. an exception will be raised. In production, - # it will dynamically compute the value so things don't break. + # By default, in development/test/etc. an exception will be raised. def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block) hashed_tag( :script, @@ -70,16 +69,12 @@ def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block) content = capture(&block) file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb') - if hashes.nil? - raise UnexpectedHashedScriptException.new(unexpected_hash_error_message(file_path, content)) - end + if raise_error_on_unrecognized_hash + hash_value = hash_source(content) + message = unexpected_hash_error_message(file_path, content, hash_value) - if hashes[file_path].nil? - message = unexpected_hash_error_message(file_path, content) - if raise_error_on_unrecognized_hash + if hashes.nil? || hashes[file_path].nil? || !hashes[file_path].include?(hash_value) raise UnexpectedHashedScriptException.new(message) - else - warn message end end @@ -88,8 +83,7 @@ def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block) content_tag type, content end - def unexpected_hash_error_message(file_path, content) - hash_value = hash_source(content) + def unexpected_hash_error_message(file_path, content, hash_value) <<-EOF \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} *** #{content} diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index c62b94ab..84074a03 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -6,7 +6,7 @@ class Message < ERB def self.template <<-TEMPLATE -<% hashed_javascript_tag do %> +<% hashed_javascript_tag(raise_error_on_unrecognized_hash = true) do %> console.log(1) <% end %> @@ -67,17 +67,25 @@ module SecureHeaders describe ViewHelpers do let(:app) { lambda { |env| [200, env, "app"] } } let(:middleware) { Middleware.new(app) } + let(:request) { Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) } - it "uses view helpers" do + before(:each) do + Configuration.default do |config| + config.csp[:script_src] = %w('self') + config.csp[:style_src] = %w('self') + end + end + + it "raises an error when attempting to hash unknown content" do + expect { + Message.new(request).result + }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) + end + + it "adds known hash values to the corresponding headers when the helper is used" do begin allow(SecureRandom).to receive(:base64).and_return("abc123") - Configuration.default do |config| - config.csp[:script_src] = %w('self') - config.csp[:style_src] = %w('self') - end - request = Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) - expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8=" Configuration.instance_variable_set(:@script_hashes, "app/views/asdfs/index.html.erb" => ["'#{expected_hash}'"]) expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc=" From 87b525e7ac3ffb02cb1ca06124b7b29973c82cbf Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 14 Apr 2016 10:23:46 -1000 Subject: [PATCH 230/636] add more tests around unknown hash behavior --- spec/lib/secure_headers/view_helpers_spec.rb | 31 +++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 84074a03..2d8c432a 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -68,15 +68,35 @@ module SecureHeaders let(:app) { lambda { |env| [200, env, "app"] } } let(:middleware) { Middleware.new(app) } let(:request) { Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) } + let(:filename) { "app/views/asdfs/index.html.erb" } - before(:each) do + before(:all) do Configuration.default do |config| config.csp[:script_src] = %w('self') config.csp[:style_src] = %w('self') end end - it "raises an error when attempting to hash unknown content" do + after(:each) do + Configuration.instance_variable_set(:@script_hashes, nil) + Configuration.instance_variable_set(:@style_hashes, nil) + end + + it "raises an error when using hashed content without precomputed hashes" do + expect { + Message.new(request).result + }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) + end + + it "raises an error when using hashed content with precomputed hashes, but none for the given file" do + Configuration.instance_variable_set(:@script_hashes, filename.reverse => ["'sha256-123'"]) + expect { + Message.new(request).result + }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) + end + + it "raises an error when using previously unknown hashed content with precomputed hashes for a given file" do + Configuration.instance_variable_set(:@script_hashes, filename => ["'sha256-123'"]) expect { Message.new(request).result }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) @@ -87,9 +107,9 @@ module SecureHeaders allow(SecureRandom).to receive(:base64).and_return("abc123") expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8=" - Configuration.instance_variable_set(:@script_hashes, "app/views/asdfs/index.html.erb" => ["'#{expected_hash}'"]) + Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"]) expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc=" - Configuration.instance_variable_set(:@style_hashes, "app/views/asdfs/index.html.erb" => ["'#{expected_style_hash}'"]) + Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"]) # render erb that calls out to helpers. Message.new(request).result @@ -99,9 +119,6 @@ module SecureHeaders expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/) expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/) expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) - ensure - Configuration.instance_variable_set(:@script_hashes, nil) - Configuration.instance_variable_set(:@style_hashes, nil) end end end From 5f77446dbacd2be02018a1847807670a62df5e22 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 18 Apr 2016 11:14:58 -1000 Subject: [PATCH 231/636] changelog/version bump for 3.2.0 --- CHANGELOG.md | 117 +++++++++++++++++++++++++++++++++++++++++ secure_headers.gemspec | 2 +- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b050d41..7415653c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,120 @@ +## 3.2.0 Cookie settings and CSP hash sources + +### Cookies + +SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. + +__Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. + +#### Boolean-based configuration + +Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. + +```ruby +config.cookies = { + secure: true, # mark all cookies as Secure + httponly: false, # do not mark any cookies as HttpOnly +} +``` + +#### Hash-based configuration + +Hash-based configuration allows for fine-grained control. + +```ruby +config.cookies = { + secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure + httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly +} +``` + +#### SameSite cookie configuration + +SameSite cookies permit either `Strict` or `Lax` enforcement mode options. + +```ruby +config.cookies = { + samesite: { + strict: true # mark all cookies as SameSite=Strict + } +} +``` + +`Strict` and `Lax` enforcement modes can also be specified using a Hash. + +```ruby +config.cookies = { + samesite: { + strict: { only: ['_rails_session'] }, + lax: { only: ['_guest'] } + } +} +``` + +#### Hash + +`script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. + +You can add hash sources directly to your policy : + +```ruby +::SecureHeaders::Configuration.default do |config| + config.csp = { + default_src: %w('self') + + # this is a made up value but browsers will show the expected hash in the console. + script_src: %w(sha256-123456) + } + end + ``` + + You can also use the automated inline script detection/collection/computation of hash source values in your app. + + ```bash + rake secure_headers:generate_hashes + ``` + + This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. + +```yaml +--- +scripts: + app/views/asdfs/index.html.erb: + - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='" +styles: + app/views/asdfs/index.html.erb: + - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" + - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" +``` + +##### Helpers + +**This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. + +```erb +<%= hashed_style_tag do %> +body { + background-color: black; +} +<% end %> + +<%= hashed_style_tag do %> +body { + font-size: 30px; + font-color: green; +} +<% end %> + +<%= hashed_javascript_tag do %> +console.log(1) +<% end %> +``` + +``` +Content-Security-Policy: ... + script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; + style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; + ## 3.1.2 Bug fix for regression See https://github.com/twitter/secureheaders/pull/239 diff --git a/secure_headers.gemspec b/secure_headers.gemspec index af75980d..c1c16f3b 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.1.2" + gem.version = "3.2.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 79f0b9cf3635cdf42f016ac8ececb4122b7331a2 Mon Sep 17 00:00:00 2001 From: stve Date: Mon, 18 Apr 2016 21:28:18 -0400 Subject: [PATCH 232/636] add a top-level test for validation cookie configuration --- spec/lib/secure_headers_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index db956e83..cbeee7da 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -312,6 +312,14 @@ module SecureHeaders end end.to raise_error(PublicKeyPinsConfigError) end + + it "validates your cookies config upon configuration" do + expect do + Configuration.default do |config| + config.cookies = { secure: "lol" } + end + end.to raise_error(CookiesConfigError) + end end end end From 74279a894f18beb3534f6d69a11dcb133cda2479 Mon Sep 17 00:00:00 2001 From: stve Date: Mon, 18 Apr 2016 21:31:02 -0400 Subject: [PATCH 233/636] add a test for referencing an undefined override --- spec/lib/secure_headers_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index cbeee7da..b64a1b5c 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -20,6 +20,13 @@ module SecureHeaders end.to raise_error(Configuration::NotYetConfiguredError) end + it "raises and ArgumentError when referencing an override that has not been set" do + expect do + Configuration.default + SecureHeaders.use_secure_headers_override(request, :missing) + end.to raise_error(ArgumentError) + end + describe "#header_hash_for" do it "allows you to opt out of individual headers via API" do Configuration.default From 84a001b10fc2bd06f50a07d28075aef50e39cb8c Mon Sep 17 00:00:00 2001 From: Ivan Reese Date: Tue, 19 Apr 2016 12:50:44 -0600 Subject: [PATCH 234/636] Fix a formatting error in the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7415653c..2ca3ab4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,7 @@ console.log(1) Content-Security-Policy: ... script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; +``` ## 3.1.2 Bug fix for regression From 10b3074bf8d45e3cd89bc3341a74e010070493f6 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 21 Apr 2016 14:41:10 -1000 Subject: [PATCH 235/636] Warn if HPKP report host is the same as the current host --- README.md | 4 +-- lib/secure_headers/configuration.rb | 29 +++++++++++++++++++--- lib/secure_headers/middleware.rb | 6 +++++ spec/lib/secure_headers/middleware_spec.rb | 17 +++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d987cb9c..ed8b0646 100644 --- a/README.md +++ b/README.md @@ -330,9 +330,7 @@ config.hpkp = { {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} ], report_only: true, # defaults to false (report-only mode) - report_uri: 'https://report-uri.io/example-hpkp', - app_name: 'example', - tag_report_uri: true + report_uri: 'https://report-uri.io/example-hpkp' } ``` diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 3c0ae95f..f6ef407f 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -61,6 +61,7 @@ def add_configuration(name, config) config.validate_config! @configurations ||= {} config.send(:cache_headers!) + config.send(:cache_hpkp_report_host) config.freeze @configurations[name] = config end @@ -102,11 +103,12 @@ def deep_copy_if_hash(value) end end + attr_accessor :dynamic_csp + attr_writer :hsts, :x_frame_options, :x_content_type_options, - :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, - :hpkp, :dynamic_csp, :cookies + :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies - attr_reader :cached_headers, :csp, :dynamic_csp, :cookies + attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml" if File.exists?(HASH_CONFIG_FILE) @@ -137,6 +139,7 @@ def dup copy.x_download_options = @x_download_options copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies copy.hpkp = @hpkp + copy.hpkp_report_host = @hpkp_report_host copy end @@ -199,12 +202,32 @@ def csp=(new_csp) @csp = new_csp end + def cookies=(cookies) + @cookies = cookies + end + def cached_headers=(headers) @cached_headers = headers end + def hpkp=(hpkp) + @hpkp = hpkp + end + + def hpkp_report_host=(hpkp_report_host) + @hpkp_report_host = hpkp_report_host + end + private + def cache_hpkp_report_host + has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri] + self.hpkp_report_host = if has_report_uri + parsed_report_uri = URI.parse(@hpkp[:report_uri]) + parsed_report_uri.host + end + end + # Public: Precompute the header names and values for this configuraiton. # Ensures that headers generated at configure time, not on demand. # diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 84013302..20f59e55 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -1,5 +1,7 @@ module SecureHeaders class Middleware + HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166" + def initialize(app) @app = app end @@ -10,6 +12,10 @@ def call(env) status, headers, response = @app.call(env) config = SecureHeaders.config_for(req) + if config.hpkp_report_host == req.host + Kernel.warn(HPKP_SAME_HOST_WARNING) + end + flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 7f45a43e..b0e6dcf3 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -13,6 +13,23 @@ module SecureHeaders Configuration.default end + it "warns if the hpkp report-uri host is the same as the current host" do + Configuration.default do |config| + config.hpkp = { + max_age: 10000000, + pins: [ + {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, + {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} + ], + report_uri: 'https://report-uri.io/example-hpkp' + } + end + + expect(Kernel).to receive(:warn).with(Middleware::HPKP_SAME_HOST_WARNING) + + middleware.call(Rack::MockRequest.env_for("https://report-uri.io", {})) + end + it "sets the headers" do _, env = middleware.call(Rack::MockRequest.env_for("https://looocalhost", {})) expect_default_values(env) From ef2d12380ca9ee059775d6476bc411d13b766b7e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 21 Apr 2016 14:46:22 -1000 Subject: [PATCH 236/636] extract report_host for clarity --- spec/lib/secure_headers/middleware_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index b0e6dcf3..a84e288e 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -14,6 +14,7 @@ module SecureHeaders end it "warns if the hpkp report-uri host is the same as the current host" do + report_host = "report-uri.io" Configuration.default do |config| config.hpkp = { max_age: 10000000, @@ -21,13 +22,13 @@ module SecureHeaders {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} ], - report_uri: 'https://report-uri.io/example-hpkp' + report_uri: "https://#{report_host}/example-hpkp" } end expect(Kernel).to receive(:warn).with(Middleware::HPKP_SAME_HOST_WARNING) - middleware.call(Rack::MockRequest.env_for("https://report-uri.io", {})) + middleware.call(Rack::MockRequest.env_for("https://#{report_host}", {})) end it "sets the headers" do From 5908b0b14bb66d4b253877758cebfc7d8bd4fc33 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 21 Apr 2016 14:56:44 -1000 Subject: [PATCH 237/636] filter unsupported Edge directives --- lib/secure_headers/headers/policy_management.rb | 2 ++ .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ spec/spec_helper.rb | 1 + 3 files changed, 8 insertions(+) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index f78f5ffb..3dfa9ed4 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -91,6 +91,7 @@ def self.included(base) UPGRADE_INSECURE_REQUESTS ].freeze + EDGE_DIRECTIVES = DIRECTIVES_1_0 SAFARI_DIRECTIVES = DIRECTIVES_1_0 FIREFOX_UNSUPPORTED_DIRECTIVES = [ @@ -118,6 +119,7 @@ def self.included(base) "Opera" => CHROME_DIRECTIVES, "Firefox" => FIREFOX_DIRECTIVES, "Safari" => SAFARI_DIRECTIVES, + "Edge" => EDGE_DIRECTIVES, "Other" => CHROME_DIRECTIVES }.freeze diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 04e60fd9..328f3de2 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -104,6 +104,11 @@ module SecureHeaders expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") end + it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do + policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge]) + expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'") + end + it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b74b1ca8..266bea01 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,6 +11,7 @@ USER_AGENTS = { + edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1', chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5', ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)', From b9de3f0a8f580c9cc66b1133f3834339599b9c3d Mon Sep 17 00:00:00 2001 From: Tom Gilligan Date: Mon, 25 Apr 2016 22:30:35 +1000 Subject: [PATCH 238/636] Add support for Referrer Policy header --- README.md | 2 + lib/secure_headers.rb | 2 + lib/secure_headers/configuration.rb | 4 +- lib/secure_headers/headers/referrer_policy.rb | 33 ++++++++++++ lib/secure_headers/railtie.rb | 2 +- .../headers/referrer_policy_spec.rb | 54 +++++++++++++++++++ spec/lib/secure_headers_spec.rb | 8 +++ spec/spec_helper.rb | 1 + 8 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 lib/secure_headers/headers/referrer_policy.rb create mode 100644 spec/lib/secure_headers/headers/referrer_policy_spec.rb diff --git a/README.md b/README.md index d987cb9c..f797d0ae 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The gem will automatically apply several headers that are related to security. - X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) +- Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so). @@ -44,6 +45,7 @@ SecureHeaders::Configuration.default do |config| config.x_xss_protection = "1; mode=block" config.x_download_options = "noopen" config.x_permitted_cross_domain_policies = "none" + config.referrer_policy = "origin-when-cross-origin" config.csp = { # "meta" values. these will shaped the header, but the values are not included in the header. report_only: true, # default: false diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 25380cde..badfdfd0 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -9,6 +9,7 @@ require "secure_headers/headers/x_content_type_options" require "secure_headers/headers/x_download_options" require "secure_headers/headers/x_permitted_cross_domain_policies" +require "secure_headers/headers/referrer_policy" require "secure_headers/middleware" require "secure_headers/railtie" require "secure_headers/view_helper" @@ -27,6 +28,7 @@ module SecureHeaders ContentSecurityPolicy, StrictTransportSecurity, PublicKeyPins, + ReferrerPolicy, XContentTypeOptions, XDownloadOptions, XFrameOptions, diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 3c0ae95f..66598f19 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -104,7 +104,7 @@ def deep_copy_if_hash(value) attr_writer :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, - :hpkp, :dynamic_csp, :cookies + :hpkp, :dynamic_csp, :cookies, :referrer_policy attr_reader :cached_headers, :csp, :dynamic_csp, :cookies @@ -136,6 +136,7 @@ def dup copy.x_xss_protection = @x_xss_protection copy.x_download_options = @x_download_options copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies + copy.referrer_policy = @referrer_policy copy.hpkp = @hpkp copy end @@ -175,6 +176,7 @@ def current_csp def validate_config! StrictTransportSecurity.validate_config!(@hsts) ContentSecurityPolicy.validate_config!(@csp) + ReferrerPolicy.validate_config!(@referrer_policy) XFrameOptions.validate_config!(@x_frame_options) XContentTypeOptions.validate_config!(@x_content_type_options) XXssProtection.validate_config!(@x_xss_protection) diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb new file mode 100644 index 00000000..7f433aa9 --- /dev/null +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -0,0 +1,33 @@ +module SecureHeaders + class ReferrerPolicyConfigError < StandardError; end + class ReferrerPolicy + HEADER_NAME = "Referrer-Policy" + DEFAULT_VALUE = "origin-when-cross-origin" + VALID_POLICIES = %w( + no-referrer + no-referrer-when-downgrade + origin + origin-when-cross-origin + unsafe-url + ) + CONFIG_KEY = :referrer_policy + + class << self + # Public: generate an Referrer Policy header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def make_header(config = nil) + [HEADER_NAME, config || DEFAULT_VALUE] + end + + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless VALID_POLICIES.include?(config.downcase) + raise ReferrerPolicyConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") + end + end + end + end +end diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index 186bc475..26a1e05b 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -7,7 +7,7 @@ class Railtie < Rails::Railtie 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options', 'X-Content-Type-Options', 'Strict-Transport-Security', 'Content-Security-Policy', 'Content-Security-Policy-Report-Only', - 'Public-Key-Pins', 'Public-Key-Pins-Report-Only'] + 'Public-Key-Pins', 'Public-Key-Pins-Report-Only, Referrer-Policy'] initializer "secure_headers.middleware" do Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware diff --git a/spec/lib/secure_headers/headers/referrer_policy_spec.rb b/spec/lib/secure_headers/headers/referrer_policy_spec.rb new file mode 100644 index 00000000..a3a9f6dd --- /dev/null +++ b/spec/lib/secure_headers/headers/referrer_policy_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +module SecureHeaders + describe ReferrerPolicy do + specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) } + specify { expect(ReferrerPolicy.make_header('no-referrer')).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) } + + context "valid configuration values" do + it "accepts 'no-referrer'" do + expect do + ReferrerPolicy.validate_config!("no-referrer") + end.not_to raise_error + end + + it "accepts 'no-referrer-when-downgrade'" do + expect do + ReferrerPolicy.validate_config!("no-referrer-when-downgrade") + end.not_to raise_error + end + + it "accepts 'origin'" do + expect do + ReferrerPolicy.validate_config!("origin") + end.not_to raise_error + end + + it "accepts 'origin-when-cross-origin'" do + expect do + ReferrerPolicy.validate_config!("origin-when-cross-origin") + end.not_to raise_error + end + + it "accepts 'unsafe-url'" do + expect do + ReferrerPolicy.validate_config!("unsafe-url") + end.not_to raise_error + end + + it "accepts nil" do + expect do + ReferrerPolicy.validate_config!(nil) + end.not_to raise_error + end + end + + context 'invlaid configuration values' do + it "doesn't accept invalid values" do + expect do + ReferrerPolicy.validate_config!("open") + end.to raise_error(ReferrerPolicyConfigError) + end + end + end +end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index b64a1b5c..b81b821f 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -312,6 +312,14 @@ module SecureHeaders end.to raise_error(XPCDPConfigError) end + it "validates your referrer_policy config upon configuration" do + expect do + Configuration.default do |config| + config.referrer_policy = "lol" + end + end.to raise_error(ReferrerPolicyConfigError) + end + it "validates your hpkp config upon configuration" do expect do Configuration.default do |config| diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b74b1ca8..1afa8d26 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -30,6 +30,7 @@ def expect_default_values(hash) expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE) expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE) + expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to eq(SecureHeaders::ReferrerPolicy::DEFAULT_VALUE) end module SecureHeaders From 1fe3cdf576cabc90693f1b9da811da0c331b39c6 Mon Sep 17 00:00:00 2001 From: Tom Gilligan Date: Tue, 26 Apr 2016 09:24:33 +1000 Subject: [PATCH 239/636] Correct a typo in name of Referrer Policy header --- lib/secure_headers/railtie.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index 26a1e05b..03df7e87 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -7,7 +7,7 @@ class Railtie < Rails::Railtie 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options', 'X-Content-Type-Options', 'Strict-Transport-Security', 'Content-Security-Policy', 'Content-Security-Policy-Report-Only', - 'Public-Key-Pins', 'Public-Key-Pins-Report-Only, Referrer-Policy'] + 'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy'] initializer "secure_headers.middleware" do Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware From 07c9520371c29d75fe3d692c1d456db5c1225ef9 Mon Sep 17 00:00:00 2001 From: Tom Gilligan Date: Tue, 26 Apr 2016 09:28:47 +1000 Subject: [PATCH 240/636] Disable Referrer-Policy by default for now --- lib/secure_headers/configuration.rb | 1 + spec/spec_helper.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 66598f19..b7332691 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -117,6 +117,7 @@ def deep_copy_if_hash(value) def initialize(&block) self.hpkp = OPT_OUT + self.referrer_policy = OPT_OUT self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG) instance_eval &block if block_given? end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1afa8d26..30050bb2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -30,7 +30,7 @@ def expect_default_values(hash) expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE) expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE) - expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to eq(SecureHeaders::ReferrerPolicy::DEFAULT_VALUE) + expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil end module SecureHeaders From eff3da9721aae64c25d31b890a2817c21907af96 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 29 Apr 2016 11:07:37 -1000 Subject: [PATCH 241/636] 3.3.0 version bump and docs --- CHANGELOG.md | 8 ++++++++ secure_headers.gemspec | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca3ab4d..23df246d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 3.3.0 referrer-policy support + +While not officially part of the spec and not implemented anywhere, support for the experimental [`referrer-policy` header](https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header) was [preemptively added](https://github.com/twitter/secureheaders/pull/249). + +Additionally, two minor enhancements were added this version: +1. [Warn when the HPKP report host is the same as the current host](https://github.com/twitter/secureheaders/pull/246). By definition any generated reports would be reporting to a known compromised connection. +1. [Filter unsupported CSP directives when using Edge](https://github.com/twitter/secureheaders/pull/247). Previously, this was causing many warnings in the developer console. + ## 3.2.0 Cookie settings and CSP hash sources ### Cookies diff --git a/secure_headers.gemspec b/secure_headers.gemspec index c1c16f3b..38b10543 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.2.0" + gem.version = "3.3.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From e8f51038759fd4ad71cf4a3815ac4a68105b9e8e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sat, 7 May 2016 09:12:56 -1000 Subject: [PATCH 242/636] Mention preserve_schemes per https://github.com/twitter/secureheaders/issues/251#issuecomment-217595827 --- upgrading-to-3-0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/upgrading-to-3-0.md b/upgrading-to-3-0.md index 0317a796..af6d0d19 100644 --- a/upgrading-to-3-0.md +++ b/upgrading-to-3-0.md @@ -13,6 +13,7 @@ Changes | `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | | Per-action configuration | override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) | | CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` | +| schemes in source expressions | Schemes were not stripped | Schemes are stripped by default to discourage mixed content. Setting `preserve_schemes: true` will revert to previous behavior | Migrating to 3.x from <= 2.x == From 89cdbf177e64da5eb1b9abbd6420cb7f80992d81 Mon Sep 17 00:00:00 2001 From: Stefan Sundin Date: Sun, 8 May 2016 13:23:43 -0700 Subject: [PATCH 243/636] Fix minor papercuts. Add description to rake task to make it appear in "rake -T". --- README.md | 22 +++++++++++----------- lib/tasks/tasks.rake | 1 + 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e0b38e48..120d15ec 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ SecureHeaders::Configuration.default do |config| config.referrer_policy = "origin-when-cross-origin" config.csp = { # "meta" values. these will shaped the header, but the values are not included in the header. - report_only: true, # default: false + report_only: true, # default: false preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives @@ -66,7 +66,7 @@ SecureHeaders::Configuration.default do |config| form_action: %w('self' github.com), frame_ancestors: %w('none'), plugin_types: %w(application/x-shockwave-flash), - block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) + block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) } @@ -85,7 +85,7 @@ end ### rails 2 -For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 applications, an explicit statement is required to use the middleware component. +For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 or non-rails applications, an explicit statement is required to use the middleware component. ```ruby use SecureHeaders::Middleware @@ -137,7 +137,7 @@ class MyController < ApplicationController end ``` -By default, a noop configuration is provided. No headers will be set when this default override is used. +By default, a no-op configuration is provided. No headers will be set when this default override is used. ```ruby class MyController < ApplicationController @@ -163,12 +163,12 @@ You can override the settings for a given action by producing a temporary overri class MyController < ApplicationController def index # Append value to the source list, override 'none' values - # Produces: default-src 'self'; script-src 'self' s3.amazaonaws.com; object-src 'self' youtube.com - append_content_security_policy_directives(script_src: %w(s3.amazaonaws.com), object_src: %w('self' youtube.com)) + # Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com + append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com)) # Overrides the previously set source list, override 'none' values - # Produces: default-src 'self'; script-src s3.amazaonaws.com; object-src 'self' - override_content_security_policy_directives(script_src: %w(s3.amazaonaws.com), object_src: %w('self')) + # Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self' + override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self')) # Global settings default to "sameorigin" override_x_frame_options("DENY") @@ -207,7 +207,7 @@ You can use a view helper to automatically add nonces to script tags: ```erb <%= nonced_javascript_tag do %> -console.log("hai"); +console.log("nonced!"); <% end %> <%= nonced_style_tag do %> @@ -324,14 +324,14 @@ Be aware that pinning error reporting is governed by the same rules as everythin ```ruby config.hpkp = { - max_age: 60.days.to_i, # max_age is a required parameter + max_age: 60.days.to_i, # max_age is a required parameter include_subdomains: true, # whether or not to apply pins to subdomains # Per the spec, SHA256 hashes are the only currently supported format. pins: [ {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} ], - report_only: true, # defaults to false (report-only mode) + report_only: true, # defaults to false (report-only mode) report_uri: 'https://report-uri.io/example-hpkp' } ``` diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake index 4af1c051..9b5c54d7 100644 --- a/lib/tasks/tasks.rake +++ b/lib/tasks/tasks.rake @@ -54,6 +54,7 @@ namespace :secure_headers do hashes end + desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" task :generate_hashes do |t, args| script_hashes = { "scripts" => {}, From caa7d925fd64e504672bd1198b3130196ca2ea15 Mon Sep 17 00:00:00 2001 From: Stefan Sundin Date: Sun, 8 May 2016 14:34:11 -0700 Subject: [PATCH 244/636] Fix false booleans being included in the CSP directive. --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index b009d7cb..3f7ce1bc 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -53,7 +53,7 @@ def build_value directives.map do |directive_name| case DIRECTIVE_VALUE_TYPES[directive_name] when :boolean - symbol_to_hyphen_case(directive_name) + symbol_to_hyphen_case(directive_name) if @config[directive_name] when :string [symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ") else From 3558578fecb8a34e0e2f63a5bec2a5173d9af912 Mon Sep 17 00:00:00 2001 From: Stefan Sundin Date: Mon, 9 May 2016 13:22:03 -0700 Subject: [PATCH 245/636] Add test for boolean values. --- .../headers/content_security_policy_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 328f3de2..e3380149 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -71,6 +71,16 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org") end + it "does add a boolean directive if the value is true" do + csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: true) + expect(csp.value).to eq("default-src example.org; block-all-mixed-content; upgrade-insecure-requests") + end + + it "does not add a boolean directive if the value is false" do + csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: false) + expect(csp.value).to eq("default-src example.org; block-all-mixed-content") + end + it "deduplicates any source expressions" do csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org)) expect(csp.value).to eq("default-src example.org") From 945fbae42c431fb3cb632459a57a2432886f0ad3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 9 May 2016 15:45:31 -0500 Subject: [PATCH 246/636] version bump for 3.3.1 --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23df246d..430ef832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.3.1 bugfix for boolean CSP directives + +[@stefansundin](https://github.com/twitter/secureheaders/pull/253) noticed that supplying `false` to "boolean" CSP directives (e.g. `upgrade-insecure-requests` and `block-all-mixed-content`) would still include the value. + ## 3.3.0 referrer-policy support While not officially part of the spec and not implemented anywhere, support for the experimental [`referrer-policy` header](https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header) was [preemptively added](https://github.com/twitter/secureheaders/pull/249). diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 38b10543..77cc991f 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.3.0" + gem.version = "3.3.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 2ee39506a32f271cb7ebb4d96b8208baea0805b0 Mon Sep 17 00:00:00 2001 From: David Han Date: Mon, 9 May 2016 18:04:28 -0400 Subject: [PATCH 247/636] Add updates to the upgrade to 3.0 readme relating to nonce and opting out of default behavior --- upgrading-to-3-0.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/upgrading-to-3-0.md b/upgrading-to-3-0.md index af6d0d19..805e1cf4 100644 --- a/upgrading-to-3-0.md +++ b/upgrading-to-3-0.md @@ -3,17 +3,19 @@ Changes == -| What | < = 2.x | >= 3.0 | -|----------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block | -| All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) | -| CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings | -| CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_script_nonce` or `content_security_policy_style_nonce` -| `self`/`none` source expressions | could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` | -| `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | -| Per-action configuration | override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) | -| CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` | -| schemes in source expressions | Schemes were not stripped | Schemes are stripped by default to discourage mixed content. Setting `preserve_schemes: true` will revert to previous behavior | +| What | < = 2.x | >= 3.0 | +| ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block | +| All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) | +| CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings | +| CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_script_nonce` or `content_security_policy_style_nonce` | +| nonce is no longer a source expression | `config.csp = "'self' 'nonce'"` | Remove `'nonce'` from source expression and use [nonce helpers](https://github.com/twitter/secureheaders#nonce). | +| `self`/`none` source expressions | Could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` | +| `inline` / `eval` source expressions | Could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | +| Per-action configuration | Override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) | +| CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` | +| Schemes in source expressions | Schemes were not stripped | Schemes are stripped by default to discourage mixed content. Setting `preserve_schemes: true` will revert to previous behavior | +| Opting out of default configuration | `skip_before_filter :set_x_download_options_header` or `config.x_download_options = false` | Within default block: `config.x_download_options = SecureHeaders::OPT_OUT` | Migrating to 3.x from <= 2.x == From 8ce2928764086b312042dcefe020be05acd93ae5 Mon Sep 17 00:00:00 2001 From: Dan Kohn Date: Sat, 14 May 2016 21:40:01 -0700 Subject: [PATCH 248/636] Alternative fix for already initialized constant Signed-off-by: Dan Kohn --- lib/tasks/tasks.rake | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake index 9b5c54d7..1be2a81b 100644 --- a/lib/tasks/tasks.rake +++ b/lib/tasks/tasks.rake @@ -1,7 +1,7 @@ -INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx -INLINE_STYLE_REGEX = /(]*>)(.*?)<\/style>/mx -INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx -INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx +INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX +INLINE_STYLE_REGEX = /(]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX +INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX +INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX namespace :secure_headers do include SecureHeaders::HashHelper From 17e199d62b515b7c9a622b53214303217c872bef Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 23 May 2016 09:36:18 -1000 Subject: [PATCH 249/636] bump to 3.3.2 --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 430ef832..1abfc94f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.3.2 minor fix to silence warnings when using rake + +[@dankohn](https://github.com/twitter/secureheaders/issues/257) was seeing "already initialized" errors in his output. This change conditionally defines the constants. + ## 3.3.1 bugfix for boolean CSP directives [@stefansundin](https://github.com/twitter/secureheaders/pull/253) noticed that supplying `false` to "boolean" CSP directives (e.g. `upgrade-insecure-requests` and `block-all-mixed-content`) would still include the value. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 77cc991f..96bf0651 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.3.1" + gem.version = "3.3.2" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 59795aafae5e9f9132ea18cb14024fe83d656f3b Mon Sep 17 00:00:00 2001 From: stve Date: Wed, 25 May 2016 14:38:34 -0400 Subject: [PATCH 250/636] update x-frame-options link to RFC [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 120d15ec..c8dee559 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) - HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797) -- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options draft](https://tools.ietf.org/html/draft-ietf-websec-x-frame-options-02) +- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034) - X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx) - X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx) From d113f43a31bbf2e61d52a7790b1c641dc2196c74 Mon Sep 17 00:00:00 2001 From: Jurre Date: Thu, 2 Jun 2016 12:20:17 +0200 Subject: [PATCH 251/636] Correct usage of content_security_policy_nonce in upgrade docs --- upgrading-to-3-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upgrading-to-3-0.md b/upgrading-to-3-0.md index 805e1cf4..34d6c25f 100644 --- a/upgrading-to-3-0.md +++ b/upgrading-to-3-0.md @@ -8,7 +8,7 @@ Changes | Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block | | All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) | | CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings | -| CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_script_nonce` or `content_security_policy_style_nonce` | +| CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_nonce(:script)` or `content_security_policy_nonce(:style)` | | nonce is no longer a source expression | `config.csp = "'self' 'nonce'"` | Remove `'nonce'` from source expression and use [nonce helpers](https://github.com/twitter/secureheaders#nonce). | | `self`/`none` source expressions | Could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` | | `inline` / `eval` source expressions | Could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | From ab122c2ef291a4f6860930e61cd9c3da90f3d364 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 8 Jun 2016 12:12:26 -1000 Subject: [PATCH 252/636] wip --- .tags | 231 ++++++++++++++++++ .../headers/content_security_policy.rb | 32 ++- .../headers/policy_management.rb | 13 + .../headers/content_security_policy_spec.rb | 8 +- .../headers/policy_management_spec.rb | 3 +- spec/lib/secure_headers_spec.rb | 5 +- 6 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 .tags diff --git a/.tags b/.tags new file mode 100644 index 00000000..a8fee57d --- /dev/null +++ b/.tags @@ -0,0 +1,231 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/ +!_TAG_PROGRAM_NAME Exuberant Ctags // +!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/ +!_TAG_PROGRAM_VERSION 5.8 // +ActionController lib/secure_headers/railtie.rb /^ module ActionController$/;" m +ActionView lib/secure_headers/view_helper.rb /^module ActionView #:nodoc:$/;" m +Base lib/secure_headers/railtie.rb /^ class Base$/;" c class:ActionController +Base lib/secure_headers/view_helper.rb /^ class Base #:nodoc:$/;" c class:ActionView +ClassMethods lib/secure_headers/headers/policy_management.rb /^ module ClassMethods$/;" m class:SecureHeaders.PolicyManagement +Configuration lib/secure_headers/configuration.rb /^ class Configuration$/;" c class:SecureHeaders +Configuration spec/spec_helper.rb /^ class Configuration$/;" c class:SecureHeaders +ContentSecurityPolicy lib/secure_headers/headers/content_security_policy.rb /^ class ContentSecurityPolicy$/;" c class:SecureHeaders +ContentSecurityPolicyConfigError lib/secure_headers/headers/content_security_policy.rb /^ class ContentSecurityPolicyConfigError < StandardError; end$/;" c class:SecureHeaders +Cookie lib/secure_headers/headers/cookie.rb /^ class Cookie$/;" c class:SecureHeaders +CookiesConfig lib/secure_headers/utils/cookies_config.rb /^ class CookiesConfig$/;" c class:SecureHeaders +CookiesConfigError lib/secure_headers/headers/cookie.rb /^ class CookiesConfigError < StandardError; end$/;" c class:SecureHeaders +HashHelper lib/secure_headers/hash_helper.rb /^ module HashHelper$/;" m class:SecureHeaders +IllegalPolicyModificationError lib/secure_headers/configuration.rb /^ class IllegalPolicyModificationError < StandardError; end$/;" c class:SecureHeaders.Configuration +Message spec/lib/secure_headers/view_helpers_spec.rb /^class Message < ERB$/;" c +Middleware lib/secure_headers/middleware.rb /^ class Middleware$/;" c class:SecureHeaders +NotYetConfiguredError lib/secure_headers/configuration.rb /^ class NotYetConfiguredError < StandardError; end$/;" c class:SecureHeaders.Configuration +PolicyManagement lib/secure_headers/headers/policy_management.rb /^ module PolicyManagement$/;" m class:SecureHeaders +PublicKeyPins lib/secure_headers/headers/public_key_pins.rb /^ class PublicKeyPins$/;" c class:SecureHeaders +PublicKeyPinsConfigError lib/secure_headers/headers/public_key_pins.rb /^ class PublicKeyPinsConfigError < StandardError; end$/;" c class:SecureHeaders +Railtie lib/secure_headers/railtie.rb /^ class Railtie < Rails::Railtie$/;" c class:SecureHeaders +ReferrerPolicy lib/secure_headers/headers/referrer_policy.rb /^ class ReferrerPolicy$/;" c class:SecureHeaders +ReferrerPolicyConfigError lib/secure_headers/headers/referrer_policy.rb /^ class ReferrerPolicyConfigError < StandardError; end$/;" c class:SecureHeaders +STSConfigError lib/secure_headers/headers/strict_transport_security.rb /^ class STSConfigError < StandardError; end$/;" c class:SecureHeaders +SecureHeaders lib/secure_headers.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/configuration.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/hash_helper.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/content_security_policy.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/cookie.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/policy_management.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/public_key_pins.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/referrer_policy.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/strict_transport_security.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/x_content_type_options.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/x_download_options.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/x_frame_options.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/headers/x_xss_protection.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/middleware.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/railtie.rb /^ module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/utils/cookies_config.rb /^module SecureHeaders$/;" m +SecureHeaders lib/secure_headers/view_helper.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/configuration_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/content_security_policy_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/cookie_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/policy_management_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/public_key_pins_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/referrer_policy_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/strict_transport_security_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/x_content_type_options_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/x_download_options_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/x_frame_options_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/headers/x_xss_protection_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/middleware_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers/view_helpers_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/lib/secure_headers_spec.rb /^module SecureHeaders$/;" m +SecureHeaders spec/spec_helper.rb /^module SecureHeaders$/;" m +StrictTransportSecurity lib/secure_headers/headers/strict_transport_security.rb /^ class StrictTransportSecurity$/;" c class:SecureHeaders +UnexpectedHashedScriptException lib/secure_headers/view_helper.rb /^ class UnexpectedHashedScriptException < StandardError; end$/;" c class:SecureHeaders.ViewHelpers +ViewHelpers lib/secure_headers/view_helper.rb /^ module ViewHelpers$/;" m class:SecureHeaders +XContentTypeOptions lib/secure_headers/headers/x_content_type_options.rb /^ class XContentTypeOptions$/;" c class:SecureHeaders +XContentTypeOptionsConfigError lib/secure_headers/headers/x_content_type_options.rb /^ class XContentTypeOptionsConfigError < StandardError; end$/;" c class:SecureHeaders +XDOConfigError lib/secure_headers/headers/x_download_options.rb /^ class XDOConfigError < StandardError; end$/;" c class:SecureHeaders +XDownloadOptions lib/secure_headers/headers/x_download_options.rb /^ class XDownloadOptions$/;" c class:SecureHeaders +XFOConfigError lib/secure_headers/headers/x_frame_options.rb /^ class XFOConfigError < StandardError; end$/;" c class:SecureHeaders +XFrameOptions lib/secure_headers/headers/x_frame_options.rb /^ class XFrameOptions$/;" c class:SecureHeaders +XPCDPConfigError lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^ class XPCDPConfigError < StandardError; end$/;" c class:SecureHeaders +XPermittedCrossDomainPolicies lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^ class XPermittedCrossDomainPolicies$/;" c class:SecureHeaders +XXssProtection lib/secure_headers/headers/x_xss_protection.rb /^ class XXssProtection$/;" c class:SecureHeaders +XXssProtectionConfigError lib/secure_headers/headers/x_xss_protection.rb /^ class XXssProtectionConfigError < StandardError; end$/;" c class:SecureHeaders +add_configuration lib/secure_headers/configuration.rb /^ def add_configuration(name, config)$/;" f class:SecureHeaders.Configuration +add_noop_configuration lib/secure_headers/configuration.rb /^ def add_noop_configuration$/;" f class:SecureHeaders.Configuration +already_flagged? lib/secure_headers/headers/cookie.rb /^ def already_flagged?(attribute)$/;" f class:SecureHeaders +append_content_security_policy_directives lib/secure_headers.rb /^ def append_content_security_policy_directives(request, additions)$/;" f class:SecureHeaders +append_content_security_policy_directives lib/secure_headers.rb /^ def append_content_security_policy_directives(additions)$/;" f +append_nonce lib/secure_headers/headers/content_security_policy.rb /^ def append_nonce(source_list, nonce)$/;" f class:build_value +boolean? lib/secure_headers/headers/policy_management.rb /^ def boolean?(source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions +build_directive lib/secure_headers/headers/content_security_policy.rb /^ def build_directive(directive)$/;" f class:build_value +build_value lib/secure_headers/headers/content_security_policy.rb /^ def build_value$/;" f +cache_headers! lib/secure_headers/configuration.rb /^ def cache_headers!$/;" f +cache_hpkp_report_host lib/secure_headers/configuration.rb /^ def cache_hpkp_report_host$/;" f +cached_headers= lib/secure_headers/configuration.rb /^ def cached_headers=(headers)$/;" f +call lib/secure_headers/middleware.rb /^ def call(env)$/;" f class:SecureHeaders.Middleware +capture spec/lib/secure_headers/view_helpers_spec.rb /^ def capture(*args)$/;" f class:Message +capture_warning spec/spec_helper.rb /^def capture_warning$/;" f +clear_configurations spec/spec_helper.rb /^ def clear_configurations$/;" f class:SecureHeaders.Configuration +combine_policies lib/secure_headers/headers/policy_management.rb /^ def combine_policies(original, additions)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods +conditionally_flag? lib/secure_headers/headers/cookie.rb /^ def conditionally_flag?(configuration)$/;" f class:SecureHeaders +config_for lib/secure_headers.rb /^ def config_for(request)$/;" f class:SecureHeaders +content_security_policy_nonce lib/secure_headers.rb /^ def content_security_policy_nonce(request, script_or_style)$/;" f class:SecureHeaders +content_security_policy_nonce lib/secure_headers/view_helper.rb /^ def content_security_policy_nonce(type)$/;" f class:SecureHeaders.ViewHelpers +content_security_policy_script_nonce lib/secure_headers.rb /^ def content_security_policy_script_nonce(request)$/;" f class:SecureHeaders +content_security_policy_script_nonce lib/secure_headers.rb /^ def content_security_policy_script_nonce$/;" f +content_security_policy_style_nonce lib/secure_headers.rb /^ def content_security_policy_style_nonce(request)$/;" f class:SecureHeaders +content_security_policy_style_nonce lib/secure_headers.rb /^ def content_security_policy_style_nonce$/;" f +content_tag spec/lib/secure_headers/view_helpers_spec.rb /^ def content_tag(type, content = nil, options = nil, &block)$/;" f class:Message +cookies= lib/secure_headers/configuration.rb /^ def cookies=(cookies)$/;" f +csp= lib/secure_headers/configuration.rb /^ def csp=(new_csp)$/;" f +csp_header_for_ua lib/secure_headers.rb /^ def csp_header_for_ua(headers, request)$/;" f +current_csp lib/secure_headers/configuration.rb /^ def current_csp$/;" f +dedup_source_list lib/secure_headers/headers/content_security_policy.rb /^ def dedup_source_list(sources)$/;" f class:build_value +deep_copy lib/secure_headers/configuration.rb /^ def deep_copy(config)$/;" f class:SecureHeaders.Configuration +deep_copy_if_hash lib/secure_headers/configuration.rb /^ def deep_copy_if_hash(value)$/;" f class:SecureHeaders +default lib/secure_headers/configuration.rb /^ def default(&block)$/;" f class:SecureHeaders.Configuration +directives lib/secure_headers/headers/content_security_policy.rb /^ def directives$/;" f class:build_value +dup lib/secure_headers/configuration.rb /^ def dup$/;" f +ensure_array_of_strings! lib/secure_headers/headers/policy_management.rb /^ def ensure_array_of_strings!(directive, source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions +ensure_valid_directive! lib/secure_headers/headers/policy_management.rb /^ def ensure_valid_directive!(directive)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions +ensure_valid_sources! lib/secure_headers/headers/policy_management.rb /^ def ensure_valid_sources!(directive, source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions +expect_default_values spec/spec_helper.rb /^def expect_default_values(hash)$/;" f +flag_cookie? lib/secure_headers/headers/cookie.rb /^ def flag_cookie?(attribute)$/;" f class:SecureHeaders +flag_cookies! lib/secure_headers/middleware.rb /^ def flag_cookies!(headers, config)$/;" f class:SecureHeaders.Middleware +flag_samesite? lib/secure_headers/headers/cookie.rb /^ def flag_samesite?$/;" f class:SecureHeaders +flag_samesite_enforcement? lib/secure_headers/headers/cookie.rb /^ def flag_samesite_enforcement?(mode)$/;" f class:SecureHeaders +flag_samesite_lax? lib/secure_headers/headers/cookie.rb /^ def flag_samesite_lax?$/;" f class:SecureHeaders +flag_samesite_strict? lib/secure_headers/headers/cookie.rb /^ def flag_samesite_strict?$/;" f class:SecureHeaders +generate_csp_headers lib/secure_headers/configuration.rb /^ def generate_csp_headers(headers)$/;" f +get lib/secure_headers/configuration.rb /^ def get(name = DEFAULT_CONFIG)$/;" f class:SecureHeaders.Configuration +hash_source lib/secure_headers/hash_helper.rb /^ def hash_source(inline_script, digest = :SHA256)$/;" f class:SecureHeaders.HashHelper +hashed_javascript_tag lib/secure_headers/view_helper.rb /^ def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block)$/;" f class:SecureHeaders.ViewHelpers +hashed_style_tag lib/secure_headers/view_helper.rb /^ def hashed_style_tag(raise_error_on_unrecognized_hash = nil, &block)$/;" f class:SecureHeaders.ViewHelpers +hashed_tag lib/secure_headers/view_helper.rb /^ def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block)$/;" f class:SecureHeaders.ViewHelpers +header_classes_for lib/secure_headers.rb /^ def header_classes_for(request)$/;" f class:SecureHeaders +header_hash_for lib/secure_headers.rb /^ def header_hash_for(request)$/;" f class:SecureHeaders +hpkp= lib/secure_headers/configuration.rb /^ def hpkp=(hpkp)$/;" f +hpkp_report_host= lib/secure_headers/configuration.rb /^ def hpkp_report_host=(hpkp_report_host)$/;" f +httponly? lib/secure_headers/headers/cookie.rb /^ def httponly?$/;" f class:SecureHeaders +idempotent_additions? lib/secure_headers/headers/policy_management.rb /^ def idempotent_additions?(config, additions)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods +included lib/secure_headers/headers/policy_management.rb /^ def self.included(base)$/;" F class:SecureHeaders.PolicyManagement +initialize lib/secure_headers/configuration.rb /^ def initialize(&block)$/;" f +initialize lib/secure_headers/headers/content_security_policy.rb /^ def initialize(config = nil, user_agent = OTHER)$/;" f class:SecureHeaders.ContentSecurityPolicy +initialize lib/secure_headers/headers/cookie.rb /^ def initialize(cookie, config)$/;" f class:SecureHeaders +initialize lib/secure_headers/headers/public_key_pins.rb /^ def initialize(config)$/;" f class:SecureHeaders +initialize lib/secure_headers/middleware.rb /^ def initialize(app)$/;" f class:SecureHeaders.Middleware +initialize lib/secure_headers/utils/cookies_config.rb /^ def initialize(config)$/;" f class:SecureHeaders.CookiesConfig +initialize spec/lib/secure_headers/view_helpers_spec.rb /^ def initialize(request, options = {})$/;" f class:Message +is_boolean? lib/secure_headers/utils/cookies_config.rb /^ def is_boolean?(obj)$/;" f class:SecureHeaders.CookiesConfig +is_hash? lib/secure_headers/utils/cookies_config.rb /^ def is_hash?(obj)$/;" f class:SecureHeaders.CookiesConfig +keep_wildcard_sources lib/secure_headers/headers/content_security_policy.rb /^ def keep_wildcard_sources(source_list)$/;" f class:build_value +make_header lib/secure_headers.rb /^ def make_header(klass, header_config, user_agent = nil)$/;" f +make_header lib/secure_headers/headers/policy_management.rb /^ def make_header(config, user_agent)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods +make_header lib/secure_headers/headers/public_key_pins.rb /^ def make_header(config)$/;" f class:SecureHeaders.PublicKeyPins +make_header lib/secure_headers/headers/referrer_policy.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.ReferrerPolicy +make_header lib/secure_headers/headers/strict_transport_security.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.StrictTransportSecurity +make_header lib/secure_headers/headers/x_content_type_options.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XContentTypeOptions +make_header lib/secure_headers/headers/x_download_options.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XDownloadOptions +make_header lib/secure_headers/headers/x_frame_options.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XFrameOptions +make_header lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XPermittedCrossDomainPolicies +make_header lib/secure_headers/headers/x_xss_protection.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XXssProtection +max_age_directive lib/secure_headers/headers/public_key_pins.rb /^ def max_age_directive$/;" f class:SecureHeaders.pin_directives +merge_policy_additions lib/secure_headers/headers/policy_management.rb /^ def merge_policy_additions(original, additions)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods +minify_source_list lib/secure_headers/headers/content_security_policy.rb /^ def minify_source_list(directive, source_list)$/;" f class:build_value +name lib/secure_headers/headers/content_security_policy.rb /^ def name$/;" f class:SecureHeaders +name lib/secure_headers/headers/public_key_pins.rb /^ def name$/;" f class:SecureHeaders +nonced_javascript_tag lib/secure_headers/view_helper.rb /^ def nonced_javascript_tag(content_or_options = {}, &block)$/;" f class:SecureHeaders.ViewHelpers +nonced_style_tag lib/secure_headers/view_helper.rb /^ def nonced_style_tag(content_or_options = {}, &block)$/;" f class:SecureHeaders.ViewHelpers +nonced_tag lib/secure_headers/view_helper.rb /^ def nonced_tag(type, content_or_options, block)$/;" f class:SecureHeaders.ViewHelpers +nonces_supported? lib/secure_headers/headers/content_security_policy.rb /^ def nonces_supported?$/;" f +normalize_child_frame_src lib/secure_headers/headers/content_security_policy.rb /^ def normalize_child_frame_src$/;" f +opt_out lib/secure_headers/configuration.rb /^ def opt_out(header)$/;" f +opt_out_of_all_protection lib/secure_headers.rb /^ def opt_out_of_all_protection(request)$/;" f class:SecureHeaders +opt_out_of_header lib/secure_headers.rb /^ def opt_out_of_header(request, header_key)$/;" f class:SecureHeaders +opt_out_of_header lib/secure_headers.rb /^ def opt_out_of_header(header_key)$/;" f +override lib/secure_headers/configuration.rb /^ def override(name, base = DEFAULT_CONFIG, &block)$/;" f class:SecureHeaders.Configuration +override_content_security_policy_directives lib/secure_headers.rb /^ def override_content_security_policy_directives(request, additions)$/;" f class:SecureHeaders +override_content_security_policy_directives lib/secure_headers.rb /^ def override_content_security_policy_directives(additions)$/;" f +override_secure lib/secure_headers/middleware.rb /^ def override_secure(env, config = {})$/;" f class:SecureHeaders.Middleware.flag_cookies! +override_secure_headers_request_config lib/secure_headers.rb /^ def override_secure_headers_request_config(request, config)$/;" f class:SecureHeaders +override_x_frame_options lib/secure_headers.rb /^ def override_x_frame_options(request, value)$/;" f class:SecureHeaders +override_x_frame_options lib/secure_headers.rb /^ def override_x_frame_options(value)$/;" f +parse lib/secure_headers/headers/cookie.rb /^ def parse(cookie)$/;" f class:SecureHeaders +parsed_cookie lib/secure_headers/headers/cookie.rb /^ def parsed_cookie$/;" f class:SecureHeaders +pin_directives lib/secure_headers/headers/public_key_pins.rb /^ def pin_directives$/;" f class:SecureHeaders +populate_fetch_source_with_default! lib/secure_headers/headers/policy_management.rb /^ def populate_fetch_source_with_default!(original, additions)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions +populate_nonces! lib/secure_headers/headers/content_security_policy.rb /^ def populate_nonces!(directive, source_list)$/;" f class:build_value +rebuild_csp_header_cache! lib/secure_headers/configuration.rb /^ def rebuild_csp_header_cache!(user_agent)$/;" f +reject_all_values_if_none! lib/secure_headers/headers/content_security_policy.rb /^ def reject_all_values_if_none!(source_list)$/;" f class:build_value +report_uri_directive lib/secure_headers/headers/public_key_pins.rb /^ def report_uri_directive$/;" f class:SecureHeaders.pin_directives +request spec/lib/secure_headers/view_helpers_spec.rb /^ def request$/;" f +reset_config spec/spec_helper.rb /^def reset_config$/;" f +result spec/lib/secure_headers/view_helpers_spec.rb /^ def result$/;" f +samesite? lib/secure_headers/headers/cookie.rb /^ def samesite?$/;" f class:SecureHeaders +samesite_cookie lib/secure_headers/headers/cookie.rb /^ def samesite_cookie$/;" f class:SecureHeaders +scheme lib/secure_headers/middleware.rb /^ def scheme(env)$/;" f class:SecureHeaders.Middleware.flag_cookies! +secure? lib/secure_headers/headers/cookie.rb /^ def secure?$/;" f class:SecureHeaders +secure_cookies= lib/secure_headers/configuration.rb /^ def secure_cookies=(secure_cookies)$/;" f +source_list? lib/secure_headers/headers/policy_management.rb /^ def source_list?(directive)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions +strip_source_schemes! lib/secure_headers/headers/content_security_policy.rb /^ def strip_source_schemes!(source_list)$/;" f class:build_value +subdomain_directive lib/secure_headers/headers/public_key_pins.rb /^ def subdomain_directive$/;" f class:SecureHeaders.pin_directives +supported_directives lib/secure_headers/headers/content_security_policy.rb /^ def supported_directives$/;" f class:build_value +symbol_to_hyphen_case lib/secure_headers/headers/content_security_policy.rb /^ def symbol_to_hyphen_case(sym)$/;" f +template spec/lib/secure_headers/view_helpers_spec.rb /^ def self.template$/;" F class:Message +to_s lib/secure_headers/headers/cookie.rb /^ def to_s$/;" f class:SecureHeaders +ua_to_variation lib/secure_headers/headers/policy_management.rb /^ def ua_to_variation(user_agent)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods +unexpected_hash_error_message lib/secure_headers/view_helper.rb /^ def unexpected_hash_error_message(file_path, content, hash_value)$/;" f class:SecureHeaders.ViewHelpers +update_x_frame_options lib/secure_headers/configuration.rb /^ def update_x_frame_options(value)$/;" f +use_cached_headers lib/secure_headers.rb /^ def use_cached_headers(headers, request)$/;" f class:SecureHeaders +use_secure_headers_override lib/secure_headers.rb /^ def use_secure_headers_override(request, name)$/;" f class:SecureHeaders +use_secure_headers_override lib/secure_headers.rb /^ def use_secure_headers_override(name)$/;" f +validate! lib/secure_headers/utils/cookies_config.rb /^ def validate!$/;" f class:SecureHeaders.CookiesConfig +validate_config! lib/secure_headers/configuration.rb /^ def validate_config!$/;" f +validate_config! lib/secure_headers/headers/cookie.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.Cookie +validate_config! lib/secure_headers/headers/policy_management.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods +validate_config! lib/secure_headers/headers/public_key_pins.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.PublicKeyPins +validate_config! lib/secure_headers/headers/referrer_policy.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.ReferrerPolicy +validate_config! lib/secure_headers/headers/strict_transport_security.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.StrictTransportSecurity +validate_config! lib/secure_headers/headers/x_content_type_options.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XContentTypeOptions +validate_config! lib/secure_headers/headers/x_download_options.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XDownloadOptions +validate_config! lib/secure_headers/headers/x_frame_options.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XFrameOptions +validate_config! lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XPermittedCrossDomainPolicies +validate_config! lib/secure_headers/headers/x_xss_protection.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XXssProtection +validate_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_config!$/;" f class:SecureHeaders.CookiesConfig +validate_directive! lib/secure_headers/headers/policy_management.rb /^ def validate_directive!(directive, source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions +validate_exclusive_use_of_hash_constraints! lib/secure_headers/utils/cookies_config.rb /^ def validate_exclusive_use_of_hash_constraints!(conf, attribute)$/;" f class:SecureHeaders.CookiesConfig +validate_exclusive_use_of_samesite_enforcement! lib/secure_headers/utils/cookies_config.rb /^ def validate_exclusive_use_of_samesite_enforcement!(attribute)$/;" f class:SecureHeaders.CookiesConfig +validate_hash_or_boolean! lib/secure_headers/utils/cookies_config.rb /^ def validate_hash_or_boolean!(attribute)$/;" f class:SecureHeaders.CookiesConfig +validate_httponly_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_httponly_config!$/;" f class:SecureHeaders.CookiesConfig +validate_samesite_boolean_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_samesite_boolean_config!$/;" f class:SecureHeaders.CookiesConfig +validate_samesite_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_samesite_config!$/;" f class:SecureHeaders.CookiesConfig +validate_samesite_hash_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_samesite_hash_config!$/;" f class:SecureHeaders.CookiesConfig +validate_secure_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_secure_config!$/;" f class:SecureHeaders.CookiesConfig +validate_source_expression! lib/secure_headers/headers/policy_management.rb /^ def validate_source_expression!(directive, source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions +value lib/secure_headers/headers/content_security_policy.rb /^ def value$/;" f class:SecureHeaders +value lib/secure_headers/headers/public_key_pins.rb /^ def value$/;" f class:SecureHeaders diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 3f7ce1bc..8a302b6b 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -1,10 +1,14 @@ require_relative 'policy_management' +require 'useragent' module SecureHeaders class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicy include PolicyManagement + # constants to be used for version-specific UA sniffing + VERSION_46 = ::UserAgent::Version.new("46") + def initialize(config = nil, user_agent = OTHER) config = Configuration.deep_copy(DEFAULT_CONFIG) unless config @config = config @@ -13,6 +17,7 @@ def initialize(config = nil, user_agent = OTHER) else UserAgent.parse(user_agent) end + normalize_child_frame_src @report_only = @config[:report_only] @preserve_schemes = @config[:preserve_schemes] @script_nonce = @config[:script_nonce] @@ -42,6 +47,21 @@ def value private + # frame-src is deprecated, child-src is being implemented. They are + # very similar and in most cases, the same value can be used for both. + def normalize_child_frame_src + Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{}") if @config[:frame_src] + + child_src = @config[:child_src] || @config[:frame_src] + if child_src + if supported_directives.include?(:child_src) + @config[:child_src] = child_src + else + @config[:frame_src] = child_src + end + end + end + # Private: converts the config object into a string representing a policy. # Places default-src at the first directive and report-uri as the last. All # others are presented in alphabetical order. @@ -162,9 +182,19 @@ def strip_source_schemes!(source_list) # Private: determine which directives are supported for the given user agent. # + # Add UA-sniffing special casing here. + # # Returns an array of symbols representing the directives. def supported_directives - @supported_directives ||= VARIATIONS[@parsed_ua.browser] || VARIATIONS[OTHER] + @supported_directives ||= if VARIATIONS[@parsed_ua.browser] + if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46 + FIREFOX_46_DIRECTIVES + else + VARIATIONS[@parsed_ua.browser] + end + else + VARIATIONS[OTHER] + end end def nonces_supported? diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 3dfa9ed4..1c290924 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -100,10 +100,23 @@ def self.included(base) PLUGIN_TYPES ].freeze + FIREFOX_46_DEPRECATED_DIRECTIVES = [ + FRAME_SRC + ].freeze + + FIREFOX_46_UNSUPPORTED_DIRECTIVES = [ + BLOCK_ALL_MIXED_CONTENT, + PLUGIN_TYPES + ].freeze + FIREFOX_DIRECTIVES = ( DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES ).freeze + FIREFOX_46_DIRECTIVES = ( + DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES + ).freeze + CHROME_DIRECTIVES = ( DIRECTIVES_2_0 + DIRECTIVES_DRAFT ).freeze diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index e3380149..e2f9c614 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -24,7 +24,7 @@ module SecureHeaders describe "#value" do it "discards 'none' values if any other source expressions are present" do - csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none'))) + csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none'))) expect(csp.value).not_to include("'none'") end @@ -88,7 +88,7 @@ module SecureHeaders context "browser sniffing" do let (:complex_opts) do - ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) do |directive, hash| + (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash| hash[directive] = %w('self') end.merge({ block_all_mixed_content: true, @@ -101,12 +101,12 @@ module SecureHeaders it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") end it "does not filter any directives for Opera" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 35e38167..1f5271f1 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -22,7 +22,7 @@ module SecureHeaders # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), - frame_src: %w('self' *.twimg.com itunes.apple.com), + child_src: %w('self' *.twimg.com itunes.apple.com), connect_src: %w(wws:), font_src: %w('self' data:), img_src: %w(mycdn.com data:), @@ -31,7 +31,6 @@ module SecureHeaders script_src: %w('self'), style_src: %w('unsafe-inline'), base_uri: %w('self'), - child_src: %w('self'), form_action: %w('self' github.com), frame_ancestors: %w('none'), plugin_types: %w(application/x-shockwave-flash), diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index b81b821f..4a07c107 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -90,8 +90,7 @@ module SecureHeaders config = Configuration.default do |config| config.csp = { default_src: %w('self'), - child_src: %w('self'), #unsupported by firefox - frame_src: %w('self') + child_src: %w('self') } end firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox])) @@ -102,6 +101,8 @@ module SecureHeaders SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self')) hash = SecureHeaders.header_hash_for(firefox_request) + + # child-src is translated to frame-src expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'") end From 66f439dfeb3e77a73dfb4af41d77ad29537f23c8 Mon Sep 17 00:00:00 2001 From: Christian Wesselhoeft Date: Fri, 10 Jun 2016 14:18:40 -0600 Subject: [PATCH 253/636] wws: -> wss: --- README.md | 2 +- spec/lib/secure_headers/headers/policy_management_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8dee559..6486cc48 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ SecureHeaders::Configuration.default do |config| # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), frame_src: %w('self' *.twimg.com itunes.apple.com), - connect_src: %w(wws:), + connect_src: %w(wss:), font_src: %w('self' data:), img_src: %w(mycdn.com data:), media_src: %w(utoob.com), diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 35e38167..6b85da3f 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -23,7 +23,7 @@ module SecureHeaders # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), frame_src: %w('self' *.twimg.com itunes.apple.com), - connect_src: %w(wws:), + connect_src: %w(wss:), font_src: %w('self' data:), img_src: %w(mycdn.com data:), media_src: %w(utoob.com), From 9a6c91381f478f7744a29d4bc3822497e290a457 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 27 Jun 2016 13:07:41 -1000 Subject: [PATCH 254/636] remove tags --- .tags | 231 ---------------------------------------------------------- 1 file changed, 231 deletions(-) delete mode 100644 .tags diff --git a/.tags b/.tags deleted file mode 100644 index a8fee57d..00000000 --- a/.tags +++ /dev/null @@ -1,231 +0,0 @@ -!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ -!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ -!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/ -!_TAG_PROGRAM_NAME Exuberant Ctags // -!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/ -!_TAG_PROGRAM_VERSION 5.8 // -ActionController lib/secure_headers/railtie.rb /^ module ActionController$/;" m -ActionView lib/secure_headers/view_helper.rb /^module ActionView #:nodoc:$/;" m -Base lib/secure_headers/railtie.rb /^ class Base$/;" c class:ActionController -Base lib/secure_headers/view_helper.rb /^ class Base #:nodoc:$/;" c class:ActionView -ClassMethods lib/secure_headers/headers/policy_management.rb /^ module ClassMethods$/;" m class:SecureHeaders.PolicyManagement -Configuration lib/secure_headers/configuration.rb /^ class Configuration$/;" c class:SecureHeaders -Configuration spec/spec_helper.rb /^ class Configuration$/;" c class:SecureHeaders -ContentSecurityPolicy lib/secure_headers/headers/content_security_policy.rb /^ class ContentSecurityPolicy$/;" c class:SecureHeaders -ContentSecurityPolicyConfigError lib/secure_headers/headers/content_security_policy.rb /^ class ContentSecurityPolicyConfigError < StandardError; end$/;" c class:SecureHeaders -Cookie lib/secure_headers/headers/cookie.rb /^ class Cookie$/;" c class:SecureHeaders -CookiesConfig lib/secure_headers/utils/cookies_config.rb /^ class CookiesConfig$/;" c class:SecureHeaders -CookiesConfigError lib/secure_headers/headers/cookie.rb /^ class CookiesConfigError < StandardError; end$/;" c class:SecureHeaders -HashHelper lib/secure_headers/hash_helper.rb /^ module HashHelper$/;" m class:SecureHeaders -IllegalPolicyModificationError lib/secure_headers/configuration.rb /^ class IllegalPolicyModificationError < StandardError; end$/;" c class:SecureHeaders.Configuration -Message spec/lib/secure_headers/view_helpers_spec.rb /^class Message < ERB$/;" c -Middleware lib/secure_headers/middleware.rb /^ class Middleware$/;" c class:SecureHeaders -NotYetConfiguredError lib/secure_headers/configuration.rb /^ class NotYetConfiguredError < StandardError; end$/;" c class:SecureHeaders.Configuration -PolicyManagement lib/secure_headers/headers/policy_management.rb /^ module PolicyManagement$/;" m class:SecureHeaders -PublicKeyPins lib/secure_headers/headers/public_key_pins.rb /^ class PublicKeyPins$/;" c class:SecureHeaders -PublicKeyPinsConfigError lib/secure_headers/headers/public_key_pins.rb /^ class PublicKeyPinsConfigError < StandardError; end$/;" c class:SecureHeaders -Railtie lib/secure_headers/railtie.rb /^ class Railtie < Rails::Railtie$/;" c class:SecureHeaders -ReferrerPolicy lib/secure_headers/headers/referrer_policy.rb /^ class ReferrerPolicy$/;" c class:SecureHeaders -ReferrerPolicyConfigError lib/secure_headers/headers/referrer_policy.rb /^ class ReferrerPolicyConfigError < StandardError; end$/;" c class:SecureHeaders -STSConfigError lib/secure_headers/headers/strict_transport_security.rb /^ class STSConfigError < StandardError; end$/;" c class:SecureHeaders -SecureHeaders lib/secure_headers.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/configuration.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/hash_helper.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/content_security_policy.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/cookie.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/policy_management.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/public_key_pins.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/referrer_policy.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/strict_transport_security.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/x_content_type_options.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/x_download_options.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/x_frame_options.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/headers/x_xss_protection.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/middleware.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/railtie.rb /^ module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/utils/cookies_config.rb /^module SecureHeaders$/;" m -SecureHeaders lib/secure_headers/view_helper.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/configuration_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/content_security_policy_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/cookie_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/policy_management_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/public_key_pins_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/referrer_policy_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/strict_transport_security_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/x_content_type_options_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/x_download_options_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/x_frame_options_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/headers/x_xss_protection_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/middleware_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers/view_helpers_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/lib/secure_headers_spec.rb /^module SecureHeaders$/;" m -SecureHeaders spec/spec_helper.rb /^module SecureHeaders$/;" m -StrictTransportSecurity lib/secure_headers/headers/strict_transport_security.rb /^ class StrictTransportSecurity$/;" c class:SecureHeaders -UnexpectedHashedScriptException lib/secure_headers/view_helper.rb /^ class UnexpectedHashedScriptException < StandardError; end$/;" c class:SecureHeaders.ViewHelpers -ViewHelpers lib/secure_headers/view_helper.rb /^ module ViewHelpers$/;" m class:SecureHeaders -XContentTypeOptions lib/secure_headers/headers/x_content_type_options.rb /^ class XContentTypeOptions$/;" c class:SecureHeaders -XContentTypeOptionsConfigError lib/secure_headers/headers/x_content_type_options.rb /^ class XContentTypeOptionsConfigError < StandardError; end$/;" c class:SecureHeaders -XDOConfigError lib/secure_headers/headers/x_download_options.rb /^ class XDOConfigError < StandardError; end$/;" c class:SecureHeaders -XDownloadOptions lib/secure_headers/headers/x_download_options.rb /^ class XDownloadOptions$/;" c class:SecureHeaders -XFOConfigError lib/secure_headers/headers/x_frame_options.rb /^ class XFOConfigError < StandardError; end$/;" c class:SecureHeaders -XFrameOptions lib/secure_headers/headers/x_frame_options.rb /^ class XFrameOptions$/;" c class:SecureHeaders -XPCDPConfigError lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^ class XPCDPConfigError < StandardError; end$/;" c class:SecureHeaders -XPermittedCrossDomainPolicies lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^ class XPermittedCrossDomainPolicies$/;" c class:SecureHeaders -XXssProtection lib/secure_headers/headers/x_xss_protection.rb /^ class XXssProtection$/;" c class:SecureHeaders -XXssProtectionConfigError lib/secure_headers/headers/x_xss_protection.rb /^ class XXssProtectionConfigError < StandardError; end$/;" c class:SecureHeaders -add_configuration lib/secure_headers/configuration.rb /^ def add_configuration(name, config)$/;" f class:SecureHeaders.Configuration -add_noop_configuration lib/secure_headers/configuration.rb /^ def add_noop_configuration$/;" f class:SecureHeaders.Configuration -already_flagged? lib/secure_headers/headers/cookie.rb /^ def already_flagged?(attribute)$/;" f class:SecureHeaders -append_content_security_policy_directives lib/secure_headers.rb /^ def append_content_security_policy_directives(request, additions)$/;" f class:SecureHeaders -append_content_security_policy_directives lib/secure_headers.rb /^ def append_content_security_policy_directives(additions)$/;" f -append_nonce lib/secure_headers/headers/content_security_policy.rb /^ def append_nonce(source_list, nonce)$/;" f class:build_value -boolean? lib/secure_headers/headers/policy_management.rb /^ def boolean?(source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions -build_directive lib/secure_headers/headers/content_security_policy.rb /^ def build_directive(directive)$/;" f class:build_value -build_value lib/secure_headers/headers/content_security_policy.rb /^ def build_value$/;" f -cache_headers! lib/secure_headers/configuration.rb /^ def cache_headers!$/;" f -cache_hpkp_report_host lib/secure_headers/configuration.rb /^ def cache_hpkp_report_host$/;" f -cached_headers= lib/secure_headers/configuration.rb /^ def cached_headers=(headers)$/;" f -call lib/secure_headers/middleware.rb /^ def call(env)$/;" f class:SecureHeaders.Middleware -capture spec/lib/secure_headers/view_helpers_spec.rb /^ def capture(*args)$/;" f class:Message -capture_warning spec/spec_helper.rb /^def capture_warning$/;" f -clear_configurations spec/spec_helper.rb /^ def clear_configurations$/;" f class:SecureHeaders.Configuration -combine_policies lib/secure_headers/headers/policy_management.rb /^ def combine_policies(original, additions)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods -conditionally_flag? lib/secure_headers/headers/cookie.rb /^ def conditionally_flag?(configuration)$/;" f class:SecureHeaders -config_for lib/secure_headers.rb /^ def config_for(request)$/;" f class:SecureHeaders -content_security_policy_nonce lib/secure_headers.rb /^ def content_security_policy_nonce(request, script_or_style)$/;" f class:SecureHeaders -content_security_policy_nonce lib/secure_headers/view_helper.rb /^ def content_security_policy_nonce(type)$/;" f class:SecureHeaders.ViewHelpers -content_security_policy_script_nonce lib/secure_headers.rb /^ def content_security_policy_script_nonce(request)$/;" f class:SecureHeaders -content_security_policy_script_nonce lib/secure_headers.rb /^ def content_security_policy_script_nonce$/;" f -content_security_policy_style_nonce lib/secure_headers.rb /^ def content_security_policy_style_nonce(request)$/;" f class:SecureHeaders -content_security_policy_style_nonce lib/secure_headers.rb /^ def content_security_policy_style_nonce$/;" f -content_tag spec/lib/secure_headers/view_helpers_spec.rb /^ def content_tag(type, content = nil, options = nil, &block)$/;" f class:Message -cookies= lib/secure_headers/configuration.rb /^ def cookies=(cookies)$/;" f -csp= lib/secure_headers/configuration.rb /^ def csp=(new_csp)$/;" f -csp_header_for_ua lib/secure_headers.rb /^ def csp_header_for_ua(headers, request)$/;" f -current_csp lib/secure_headers/configuration.rb /^ def current_csp$/;" f -dedup_source_list lib/secure_headers/headers/content_security_policy.rb /^ def dedup_source_list(sources)$/;" f class:build_value -deep_copy lib/secure_headers/configuration.rb /^ def deep_copy(config)$/;" f class:SecureHeaders.Configuration -deep_copy_if_hash lib/secure_headers/configuration.rb /^ def deep_copy_if_hash(value)$/;" f class:SecureHeaders -default lib/secure_headers/configuration.rb /^ def default(&block)$/;" f class:SecureHeaders.Configuration -directives lib/secure_headers/headers/content_security_policy.rb /^ def directives$/;" f class:build_value -dup lib/secure_headers/configuration.rb /^ def dup$/;" f -ensure_array_of_strings! lib/secure_headers/headers/policy_management.rb /^ def ensure_array_of_strings!(directive, source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions -ensure_valid_directive! lib/secure_headers/headers/policy_management.rb /^ def ensure_valid_directive!(directive)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions -ensure_valid_sources! lib/secure_headers/headers/policy_management.rb /^ def ensure_valid_sources!(directive, source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions -expect_default_values spec/spec_helper.rb /^def expect_default_values(hash)$/;" f -flag_cookie? lib/secure_headers/headers/cookie.rb /^ def flag_cookie?(attribute)$/;" f class:SecureHeaders -flag_cookies! lib/secure_headers/middleware.rb /^ def flag_cookies!(headers, config)$/;" f class:SecureHeaders.Middleware -flag_samesite? lib/secure_headers/headers/cookie.rb /^ def flag_samesite?$/;" f class:SecureHeaders -flag_samesite_enforcement? lib/secure_headers/headers/cookie.rb /^ def flag_samesite_enforcement?(mode)$/;" f class:SecureHeaders -flag_samesite_lax? lib/secure_headers/headers/cookie.rb /^ def flag_samesite_lax?$/;" f class:SecureHeaders -flag_samesite_strict? lib/secure_headers/headers/cookie.rb /^ def flag_samesite_strict?$/;" f class:SecureHeaders -generate_csp_headers lib/secure_headers/configuration.rb /^ def generate_csp_headers(headers)$/;" f -get lib/secure_headers/configuration.rb /^ def get(name = DEFAULT_CONFIG)$/;" f class:SecureHeaders.Configuration -hash_source lib/secure_headers/hash_helper.rb /^ def hash_source(inline_script, digest = :SHA256)$/;" f class:SecureHeaders.HashHelper -hashed_javascript_tag lib/secure_headers/view_helper.rb /^ def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block)$/;" f class:SecureHeaders.ViewHelpers -hashed_style_tag lib/secure_headers/view_helper.rb /^ def hashed_style_tag(raise_error_on_unrecognized_hash = nil, &block)$/;" f class:SecureHeaders.ViewHelpers -hashed_tag lib/secure_headers/view_helper.rb /^ def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block)$/;" f class:SecureHeaders.ViewHelpers -header_classes_for lib/secure_headers.rb /^ def header_classes_for(request)$/;" f class:SecureHeaders -header_hash_for lib/secure_headers.rb /^ def header_hash_for(request)$/;" f class:SecureHeaders -hpkp= lib/secure_headers/configuration.rb /^ def hpkp=(hpkp)$/;" f -hpkp_report_host= lib/secure_headers/configuration.rb /^ def hpkp_report_host=(hpkp_report_host)$/;" f -httponly? lib/secure_headers/headers/cookie.rb /^ def httponly?$/;" f class:SecureHeaders -idempotent_additions? lib/secure_headers/headers/policy_management.rb /^ def idempotent_additions?(config, additions)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods -included lib/secure_headers/headers/policy_management.rb /^ def self.included(base)$/;" F class:SecureHeaders.PolicyManagement -initialize lib/secure_headers/configuration.rb /^ def initialize(&block)$/;" f -initialize lib/secure_headers/headers/content_security_policy.rb /^ def initialize(config = nil, user_agent = OTHER)$/;" f class:SecureHeaders.ContentSecurityPolicy -initialize lib/secure_headers/headers/cookie.rb /^ def initialize(cookie, config)$/;" f class:SecureHeaders -initialize lib/secure_headers/headers/public_key_pins.rb /^ def initialize(config)$/;" f class:SecureHeaders -initialize lib/secure_headers/middleware.rb /^ def initialize(app)$/;" f class:SecureHeaders.Middleware -initialize lib/secure_headers/utils/cookies_config.rb /^ def initialize(config)$/;" f class:SecureHeaders.CookiesConfig -initialize spec/lib/secure_headers/view_helpers_spec.rb /^ def initialize(request, options = {})$/;" f class:Message -is_boolean? lib/secure_headers/utils/cookies_config.rb /^ def is_boolean?(obj)$/;" f class:SecureHeaders.CookiesConfig -is_hash? lib/secure_headers/utils/cookies_config.rb /^ def is_hash?(obj)$/;" f class:SecureHeaders.CookiesConfig -keep_wildcard_sources lib/secure_headers/headers/content_security_policy.rb /^ def keep_wildcard_sources(source_list)$/;" f class:build_value -make_header lib/secure_headers.rb /^ def make_header(klass, header_config, user_agent = nil)$/;" f -make_header lib/secure_headers/headers/policy_management.rb /^ def make_header(config, user_agent)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods -make_header lib/secure_headers/headers/public_key_pins.rb /^ def make_header(config)$/;" f class:SecureHeaders.PublicKeyPins -make_header lib/secure_headers/headers/referrer_policy.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.ReferrerPolicy -make_header lib/secure_headers/headers/strict_transport_security.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.StrictTransportSecurity -make_header lib/secure_headers/headers/x_content_type_options.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XContentTypeOptions -make_header lib/secure_headers/headers/x_download_options.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XDownloadOptions -make_header lib/secure_headers/headers/x_frame_options.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XFrameOptions -make_header lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XPermittedCrossDomainPolicies -make_header lib/secure_headers/headers/x_xss_protection.rb /^ def make_header(config = nil)$/;" f class:SecureHeaders.XXssProtection -max_age_directive lib/secure_headers/headers/public_key_pins.rb /^ def max_age_directive$/;" f class:SecureHeaders.pin_directives -merge_policy_additions lib/secure_headers/headers/policy_management.rb /^ def merge_policy_additions(original, additions)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods -minify_source_list lib/secure_headers/headers/content_security_policy.rb /^ def minify_source_list(directive, source_list)$/;" f class:build_value -name lib/secure_headers/headers/content_security_policy.rb /^ def name$/;" f class:SecureHeaders -name lib/secure_headers/headers/public_key_pins.rb /^ def name$/;" f class:SecureHeaders -nonced_javascript_tag lib/secure_headers/view_helper.rb /^ def nonced_javascript_tag(content_or_options = {}, &block)$/;" f class:SecureHeaders.ViewHelpers -nonced_style_tag lib/secure_headers/view_helper.rb /^ def nonced_style_tag(content_or_options = {}, &block)$/;" f class:SecureHeaders.ViewHelpers -nonced_tag lib/secure_headers/view_helper.rb /^ def nonced_tag(type, content_or_options, block)$/;" f class:SecureHeaders.ViewHelpers -nonces_supported? lib/secure_headers/headers/content_security_policy.rb /^ def nonces_supported?$/;" f -normalize_child_frame_src lib/secure_headers/headers/content_security_policy.rb /^ def normalize_child_frame_src$/;" f -opt_out lib/secure_headers/configuration.rb /^ def opt_out(header)$/;" f -opt_out_of_all_protection lib/secure_headers.rb /^ def opt_out_of_all_protection(request)$/;" f class:SecureHeaders -opt_out_of_header lib/secure_headers.rb /^ def opt_out_of_header(request, header_key)$/;" f class:SecureHeaders -opt_out_of_header lib/secure_headers.rb /^ def opt_out_of_header(header_key)$/;" f -override lib/secure_headers/configuration.rb /^ def override(name, base = DEFAULT_CONFIG, &block)$/;" f class:SecureHeaders.Configuration -override_content_security_policy_directives lib/secure_headers.rb /^ def override_content_security_policy_directives(request, additions)$/;" f class:SecureHeaders -override_content_security_policy_directives lib/secure_headers.rb /^ def override_content_security_policy_directives(additions)$/;" f -override_secure lib/secure_headers/middleware.rb /^ def override_secure(env, config = {})$/;" f class:SecureHeaders.Middleware.flag_cookies! -override_secure_headers_request_config lib/secure_headers.rb /^ def override_secure_headers_request_config(request, config)$/;" f class:SecureHeaders -override_x_frame_options lib/secure_headers.rb /^ def override_x_frame_options(request, value)$/;" f class:SecureHeaders -override_x_frame_options lib/secure_headers.rb /^ def override_x_frame_options(value)$/;" f -parse lib/secure_headers/headers/cookie.rb /^ def parse(cookie)$/;" f class:SecureHeaders -parsed_cookie lib/secure_headers/headers/cookie.rb /^ def parsed_cookie$/;" f class:SecureHeaders -pin_directives lib/secure_headers/headers/public_key_pins.rb /^ def pin_directives$/;" f class:SecureHeaders -populate_fetch_source_with_default! lib/secure_headers/headers/policy_management.rb /^ def populate_fetch_source_with_default!(original, additions)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions -populate_nonces! lib/secure_headers/headers/content_security_policy.rb /^ def populate_nonces!(directive, source_list)$/;" f class:build_value -rebuild_csp_header_cache! lib/secure_headers/configuration.rb /^ def rebuild_csp_header_cache!(user_agent)$/;" f -reject_all_values_if_none! lib/secure_headers/headers/content_security_policy.rb /^ def reject_all_values_if_none!(source_list)$/;" f class:build_value -report_uri_directive lib/secure_headers/headers/public_key_pins.rb /^ def report_uri_directive$/;" f class:SecureHeaders.pin_directives -request spec/lib/secure_headers/view_helpers_spec.rb /^ def request$/;" f -reset_config spec/spec_helper.rb /^def reset_config$/;" f -result spec/lib/secure_headers/view_helpers_spec.rb /^ def result$/;" f -samesite? lib/secure_headers/headers/cookie.rb /^ def samesite?$/;" f class:SecureHeaders -samesite_cookie lib/secure_headers/headers/cookie.rb /^ def samesite_cookie$/;" f class:SecureHeaders -scheme lib/secure_headers/middleware.rb /^ def scheme(env)$/;" f class:SecureHeaders.Middleware.flag_cookies! -secure? lib/secure_headers/headers/cookie.rb /^ def secure?$/;" f class:SecureHeaders -secure_cookies= lib/secure_headers/configuration.rb /^ def secure_cookies=(secure_cookies)$/;" f -source_list? lib/secure_headers/headers/policy_management.rb /^ def source_list?(directive)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions -strip_source_schemes! lib/secure_headers/headers/content_security_policy.rb /^ def strip_source_schemes!(source_list)$/;" f class:build_value -subdomain_directive lib/secure_headers/headers/public_key_pins.rb /^ def subdomain_directive$/;" f class:SecureHeaders.pin_directives -supported_directives lib/secure_headers/headers/content_security_policy.rb /^ def supported_directives$/;" f class:build_value -symbol_to_hyphen_case lib/secure_headers/headers/content_security_policy.rb /^ def symbol_to_hyphen_case(sym)$/;" f -template spec/lib/secure_headers/view_helpers_spec.rb /^ def self.template$/;" F class:Message -to_s lib/secure_headers/headers/cookie.rb /^ def to_s$/;" f class:SecureHeaders -ua_to_variation lib/secure_headers/headers/policy_management.rb /^ def ua_to_variation(user_agent)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods -unexpected_hash_error_message lib/secure_headers/view_helper.rb /^ def unexpected_hash_error_message(file_path, content, hash_value)$/;" f class:SecureHeaders.ViewHelpers -update_x_frame_options lib/secure_headers/configuration.rb /^ def update_x_frame_options(value)$/;" f -use_cached_headers lib/secure_headers.rb /^ def use_cached_headers(headers, request)$/;" f class:SecureHeaders -use_secure_headers_override lib/secure_headers.rb /^ def use_secure_headers_override(request, name)$/;" f class:SecureHeaders -use_secure_headers_override lib/secure_headers.rb /^ def use_secure_headers_override(name)$/;" f -validate! lib/secure_headers/utils/cookies_config.rb /^ def validate!$/;" f class:SecureHeaders.CookiesConfig -validate_config! lib/secure_headers/configuration.rb /^ def validate_config!$/;" f -validate_config! lib/secure_headers/headers/cookie.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.Cookie -validate_config! lib/secure_headers/headers/policy_management.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods -validate_config! lib/secure_headers/headers/public_key_pins.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.PublicKeyPins -validate_config! lib/secure_headers/headers/referrer_policy.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.ReferrerPolicy -validate_config! lib/secure_headers/headers/strict_transport_security.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.StrictTransportSecurity -validate_config! lib/secure_headers/headers/x_content_type_options.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XContentTypeOptions -validate_config! lib/secure_headers/headers/x_download_options.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XDownloadOptions -validate_config! lib/secure_headers/headers/x_frame_options.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XFrameOptions -validate_config! lib/secure_headers/headers/x_permitted_cross_domain_policies.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XPermittedCrossDomainPolicies -validate_config! lib/secure_headers/headers/x_xss_protection.rb /^ def validate_config!(config)$/;" f class:SecureHeaders.XXssProtection -validate_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_config!$/;" f class:SecureHeaders.CookiesConfig -validate_directive! lib/secure_headers/headers/policy_management.rb /^ def validate_directive!(directive, source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions -validate_exclusive_use_of_hash_constraints! lib/secure_headers/utils/cookies_config.rb /^ def validate_exclusive_use_of_hash_constraints!(conf, attribute)$/;" f class:SecureHeaders.CookiesConfig -validate_exclusive_use_of_samesite_enforcement! lib/secure_headers/utils/cookies_config.rb /^ def validate_exclusive_use_of_samesite_enforcement!(attribute)$/;" f class:SecureHeaders.CookiesConfig -validate_hash_or_boolean! lib/secure_headers/utils/cookies_config.rb /^ def validate_hash_or_boolean!(attribute)$/;" f class:SecureHeaders.CookiesConfig -validate_httponly_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_httponly_config!$/;" f class:SecureHeaders.CookiesConfig -validate_samesite_boolean_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_samesite_boolean_config!$/;" f class:SecureHeaders.CookiesConfig -validate_samesite_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_samesite_config!$/;" f class:SecureHeaders.CookiesConfig -validate_samesite_hash_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_samesite_hash_config!$/;" f class:SecureHeaders.CookiesConfig -validate_secure_config! lib/secure_headers/utils/cookies_config.rb /^ def validate_secure_config!$/;" f class:SecureHeaders.CookiesConfig -validate_source_expression! lib/secure_headers/headers/policy_management.rb /^ def validate_source_expression!(directive, source_expression)$/;" f class:SecureHeaders.PolicyManagement.ClassMethods.merge_policy_additions -value lib/secure_headers/headers/content_security_policy.rb /^ def value$/;" f class:SecureHeaders -value lib/secure_headers/headers/public_key_pins.rb /^ def value$/;" f class:SecureHeaders From 697f5a9ccf49714751d1f0f1f08c2afa3c315142 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 27 Jun 2016 13:45:32 -1000 Subject: [PATCH 255/636] Add the "FirefoxTransitional" CSP variation This ensures it can be cached. --- lib/secure_headers/configuration.rb | 4 ++-- lib/secure_headers/headers/content_security_policy.rb | 7 +++---- lib/secure_headers/headers/policy_management.rb | 1 + .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ spec/spec_helper.rb | 3 ++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 8dd0dd1c..59c42c59 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -203,7 +203,7 @@ def csp=(new_csp) raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead." end - @csp = new_csp + @csp = self.class.send(:deep_copy_if_hash, new_csp) end def cookies=(cookies) @@ -215,7 +215,7 @@ def cached_headers=(headers) end def hpkp=(hpkp) - @hpkp = hpkp + @hpkp = self.class.send(:deep_copy_if_hash, hpkp) end def hpkp_report_host=(hpkp_report_host) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 8a302b6b..b1873179 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -10,8 +10,7 @@ class ContentSecurityPolicy VERSION_46 = ::UserAgent::Version.new("46") def initialize(config = nil, user_agent = OTHER) - config = Configuration.deep_copy(DEFAULT_CONFIG) unless config - @config = config + @config = Configuration.send(:deep_copy, config || DEFAULT_CONFIG) @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base) user_agent else @@ -50,7 +49,7 @@ def value # frame-src is deprecated, child-src is being implemented. They are # very similar and in most cases, the same value can be used for both. def normalize_child_frame_src - Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{}") if @config[:frame_src] + Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}") if @config[:frame_src] child_src = @config[:child_src] || @config[:frame_src] if child_src @@ -188,7 +187,7 @@ def strip_source_schemes!(source_list) def supported_directives @supported_directives ||= if VARIATIONS[@parsed_ua.browser] if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46 - FIREFOX_46_DIRECTIVES + VARIATIONS["FirefoxTransitional"] else VARIATIONS[@parsed_ua.browser] end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 1c290924..58e0eb7b 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -131,6 +131,7 @@ def self.included(base) "Chrome" => CHROME_DIRECTIVES, "Opera" => CHROME_DIRECTIVES, "Firefox" => FIREFOX_DIRECTIVES, + "FirefoxTransitional" => FIREFOX_46_DIRECTIVES, "Safari" => SAFARI_DIRECTIVES, "Edge" => EDGE_DIRECTIVES, "Other" => CHROME_DIRECTIVES diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index e2f9c614..5e898f21 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -114,6 +114,11 @@ module SecureHeaders expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") end + it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do + policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46]) + expect(policy.value).to eq("default-src 'self'; base-uri 'self'; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") + end + it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge]) expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9283888a..9658eb89 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,7 +12,8 @@ USER_AGENTS = { edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", - firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1', + firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1", + firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0", chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5', ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)', opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00', From a3aca2c71431c0602658ec0e6bea2bd00ab8323c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 27 Jun 2016 13:56:00 -1000 Subject: [PATCH 256/636] add tests showing each directives specific value. also demonstrats copying child-src to frame-src. --- .../headers/content_security_policy_spec.rb | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 5e898f21..dbbf387a 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -89,44 +89,44 @@ module SecureHeaders context "browser sniffing" do let (:complex_opts) do (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash| - hash[directive] = %w('self') + hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"] end.merge({ block_all_mixed_content: true, upgrade_insecure_requests: true, reflected_xss: "block", - script_src: %w('self'), + script_src: %w(script-src.com), script_nonce: 123456 }) end it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "does not filter any directives for Opera" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46]) - expect(policy.value).to eq("default-src 'self'; base-uri 'self'; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end - it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do + it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge]) - expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'") + expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end - it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do + it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) - expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'") + expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end end end From 6803f3b66df35441c9fa0c701ba8f198ae333754 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 27 Jun 2016 14:18:20 -1000 Subject: [PATCH 257/636] add more clarity around behavior when child and frame-src directives are supplied --- .../headers/content_security_policy.rb | 17 ++++++++------- .../headers/content_security_policy_spec.rb | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index b1873179..efee3428 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -49,15 +49,16 @@ def value # frame-src is deprecated, child-src is being implemented. They are # very similar and in most cases, the same value can be used for both. def normalize_child_frame_src - Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}") if @config[:frame_src] + if @config[:frame_src] && @config[:child_src] && @config[:frame_src] != @config[:child_src] + Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.") + elsif @config[:frame_src] + Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}.") + end - child_src = @config[:child_src] || @config[:frame_src] - if child_src - if supported_directives.include?(:child_src) - @config[:child_src] = child_src - else - @config[:frame_src] = child_src - end + if supported_directives.include?(:child_src) + @config[:child_src] = @config[:child_src] || @config[:frame_src] + else + @config[:frame_src] = @config[:frame_src] || @config[:child_src] end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index dbbf387a..1e30f62b 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -86,6 +86,27 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org") end + it "emits a warning when using frame-src" do + expect(Kernel).to receive(:warn).with(/:frame_src is deprecated, use :child_src instead./) + ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value + end + + it "emits a warning when child-src and frame-src are supplied but are not equal" do + expect(Kernel).to receive(:warn).with(/both :child_src and :frame_src supplied and do not match./) + ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value + end + + it "will still set inconsistent child/frame-src values to be less surprising" do + expect(Kernel).to receive(:warn).at_least(:once) + firefox = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox]).value + firefox_transitional = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox46]).value + expect(firefox).not_to eq(firefox_transitional) + expect(firefox).to match(/frame-src/) + expect(firefox).not_to match(/child-src/) + expect(firefox_transitional).to match(/child-src/) + expect(firefox_transitional).not_to match(/frame-src/) + end + context "browser sniffing" do let (:complex_opts) do (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash| From 883779c0fe8b14bb5c6c5f5a65ef12437bb0033a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 27 Jun 2016 14:25:03 -1000 Subject: [PATCH 258/636] remove reference to frame-src in README --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c8dee559..73b4ee55 100644 --- a/README.md +++ b/README.md @@ -53,20 +53,19 @@ SecureHeaders::Configuration.default do |config| # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), - frame_src: %w('self' *.twimg.com itunes.apple.com), + base_uri: %w('self'), + block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ + child_src: %w('self'), connect_src: %w(wws:), font_src: %w('self' data:), + form_action: %w('self' github.com), + frame_ancestors: %w('none'), img_src: %w(mycdn.com data:), media_src: %w(utoob.com), object_src: %w('self'), + plugin_types: %w(application/x-shockwave-flash), script_src: %w('self'), style_src: %w('unsafe-inline'), - base_uri: %w('self'), - child_src: %w('self'), - form_action: %w('self' github.com), - frame_ancestors: %w('none'), - plugin_types: %w(application/x-shockwave-flash), - block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) } From 6de28f23156f849218390757df2d598edb57d119 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 8 Jul 2016 12:33:08 -1000 Subject: [PATCH 259/636] lock test rack at 1.x.x --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index f1c33ce8..7dfb5b90 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ gemspec group :test do gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0 gem "pry-nav" - gem "rack" + gem "rack", "~> 1" gem "rspec" gem "coveralls" end From 951beee9cb375a5970af795227c2aaace9ed9568 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 8 Jul 2016 12:39:12 -1000 Subject: [PATCH 260/636] lock the json gem to a 1.9.3-friendly version --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 7dfb5b90..543d82cc 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ gemspec group :test do gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0 gem "pry-nav" + gem "json", "~> 1" gem "rack", "~> 1" gem "rspec" gem "coveralls" From cc281302b7901719d1030bc43ee845c10e5ced07 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 8 Jul 2016 12:52:39 -1000 Subject: [PATCH 261/636] Create ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..8f7a8940 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,41 @@ +# Feature Requests + +## Adding a new header + +Generally, adding a new header is always OK. + +* Is the header supported by any user agent? If so, which? +* What does it do? +* What are the valid values for the header? +* Where does the specification live? + +## Adding a new CSP directive + +* Is the directive supported by any user agent? If so, which? +* What does it do? +* What are the valid values for the directive? + +--- + +# Bugs + +Console errors and deprecation warnings are considered bugs that should be addressed with more precise UA sniffing. Bugs caused by incorrect or invalid UA sniffing are also bugs. + +### Expected outcome + +Describe what you expected to happen + +1. I configure CSP to do X +1. When I inspect the response headers, the CSP should have included X + +### Actual outcome + +1. The generated policy did not include X + +### Config + +Please provide the configuration (`SecureHeaders::Configuration.default`) you are using including any overrides (`SecureHeaders::Configuration.override`). + +### Generated headers + +Provide a sample response containing the headers From 51aa52edb1583c94b1de88bd39c64935bd3658d9 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 8 Jul 2016 12:54:01 -1000 Subject: [PATCH 262/636] Create PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..2ed6b844 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## Adding a new header + +Generally, adding a new header is always OK. + +* Is the header supported by any user agent? If so, which? +* What does it do? +* What are the valid values for the header? +* Where does the specification live? + +## Adding a new CSP directive + +* Is the directive supported by any user agent? If so, which? +* What does it do? +* What are the valid values for the directive? From 9bf22e772e6d84f79c97e6afbd53a46a7cf8612b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 8 Jul 2016 16:38:36 -1000 Subject: [PATCH 263/636] Add basic checklist to PR template --- .github/PULL_REQUEST_TEMPLATE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2ed6b844..c151d888 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,8 @@ +## All PRs: + +* [ ] Has tests +* [ ] Documentation updated + ## Adding a new header Generally, adding a new header is always OK. @@ -12,3 +17,4 @@ Generally, adding a new header is always OK. * Is the directive supported by any user agent? If so, which? * What does it do? * What are the valid values for the directive? + From 129720eb20fac66fd1e0730b46e76630befc227d Mon Sep 17 00:00:00 2001 From: Koen Punt Date: Wed, 13 Jul 2016 16:03:06 +0200 Subject: [PATCH 264/636] hook include of helper to loading of ActionView When using rspec with rails-controller-testing, specs would break due to incorrect load order. More info here: https://github.com/rails/rails-controller-testing/issues/24 --- lib/secure_headers/view_helper.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index de5141c8..65c1559d 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -108,8 +108,6 @@ def nonced_tag(type, content_or_options, block) end end -module ActionView #:nodoc: - class Base #:nodoc: - include SecureHeaders::ViewHelpers - end +ActiveSupport.on_load :action_view do + include SecureHeaders::ViewHelpers end From fbb4dc2cc0b470d074b481ae3b616ccdf4fd1201 Mon Sep 17 00:00:00 2001 From: Koen Punt Date: Wed, 13 Jul 2016 20:33:21 +0200 Subject: [PATCH 265/636] check for existence of ActiveSupport --- lib/secure_headers/view_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 65c1559d..049f578c 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -110,4 +110,4 @@ def nonced_tag(type, content_or_options, block) ActiveSupport.on_load :action_view do include SecureHeaders::ViewHelpers -end +end if defined?(ActiveSupport) From 526467050bd9a6b391dd34efda95d180c0998f3a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 27 Jun 2016 15:13:24 -1000 Subject: [PATCH 266/636] add report-only functionality --- lib/secure_headers.rb | 30 +++++-- lib/secure_headers/configuration.rb | 64 +++++++++++--- .../headers/policy_management.rb | 1 + spec/lib/secure_headers/view_helpers_spec.rb | 7 +- spec/lib/secure_headers_spec.rb | 84 ++++++++++++++++++- 5 files changed, 162 insertions(+), 24 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index badfdfd0..940f30dd 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -66,9 +66,19 @@ def override_content_security_policy_directives(request, additions) # # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) - def append_content_security_policy_directives(request, additions) + def append_content_security_policy_directives(request, additions, target=:both) + unless [:both, :enforced, :report_only].include?(target) + raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]" + end + config = config_for(request) - config.dynamic_csp = CSP.combine_policies(config.current_csp, additions) + if [:both, :enforced].include?(target) && config.current_csp != OPT_OUT + config.dynamic_csp = CSP.combine_policies(config.current_csp, additions) + end + if [:both, :report_only].include?(target) && config.current_csp_report_only != OPT_OUT + config.dynamic_csp_report_only = CSP.combine_policies(config.current_csp_report_only, additions) + end + override_secure_headers_request_config(request, config) end @@ -109,7 +119,11 @@ def opt_out_of_all_protection(request) def header_hash_for(request) config = config_for(request) unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp) - config.rebuild_csp_header_cache!(request.user_agent) + config.rebuild_csp_header_cache!(request.user_agent, CSP::CONFIG_KEY) + end + + unless ContentSecurityPolicy.idempotent_additions?(config.csp_report_only, config.current_csp_report_only) + config.rebuild_csp_header_cache!(request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) end use_cached_headers(config.cached_headers, request) @@ -190,7 +204,9 @@ def header_classes_for(request) ALL_HEADER_CLASSES else HTTP_HEADER_CLASSES - end + end.map do |klass| + klass::CONFIG_KEY + end.concat([CSP::REPORT_ONLY_CONFIG_KEY]) end # Private: takes a precomputed hash of headers and returns the Headers @@ -198,9 +214,9 @@ def header_classes_for(request) # # Returns a hash of header names / values valid for a given request. def use_cached_headers(headers, request) - header_classes_for(request).each_with_object({}) do |klass, hash| - if header = headers[klass::CONFIG_KEY] - header_name, value = if klass == CSP + header_classes_for(request).each_with_object({}) do |config_key, hash| + if header = headers[config_key] + header_name, value = if [CSP::CONFIG_KEY, CSP::REPORT_ONLY_CONFIG_KEY].include?(config_key) csp_header_for_ua(header, request) else header diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 59c42c59..6329fc52 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -75,6 +75,7 @@ def add_noop_configuration config.send("#{klass::CONFIG_KEY}=", OPT_OUT) end config.dynamic_csp = OPT_OUT + config.dynamic_csp_report_only = OPT_OUT end add_configuration(NOOP_CONFIGURATION, noop_config) @@ -103,13 +104,13 @@ def deep_copy_if_hash(value) end end - attr_accessor :dynamic_csp + attr_accessor :dynamic_csp, :dynamic_csp_report_only attr_writer :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, :referrer_policy - attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host + attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml" if File.exists?(HASH_CONFIG_FILE) @@ -122,6 +123,7 @@ def initialize(&block) self.hpkp = OPT_OUT self.referrer_policy = OPT_OUT self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG) + self.csp_report_only = OPT_OUT instance_eval &block if block_given? end @@ -133,6 +135,8 @@ def dup copy.cookies = @cookies copy.csp = self.class.send(:deep_copy_if_hash, @csp) copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp) + copy.csp_report_only = self.class.send(:deep_copy_if_hash, @csp_report_only) + copy.dynamic_csp_report_only = self.class.send(:deep_copy_if_hash, @dynamic_csp_report_only) copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) copy.x_content_type_options = @x_content_type_options copy.hsts = @hsts @@ -149,7 +153,9 @@ def dup def opt_out(header) send("#{header}=", OPT_OUT) if header == CSP::CONFIG_KEY - dynamic_csp = OPT_OUT + self.dynamic_csp = OPT_OUT + elsif header == CSP::REPORT_ONLY_CONFIG_KEY + self.dynamic_csp_report_only = OPT_OUT end self.cached_headers.delete(header) end @@ -160,12 +166,14 @@ def update_x_frame_options(value) end # Public: generated cached headers for a specific user agent. - def rebuild_csp_header_cache!(user_agent) - self.cached_headers[CSP::CONFIG_KEY] = {} - unless current_csp == OPT_OUT + def rebuild_csp_header_cache!(user_agent, header_key) + self.cached_headers[header_key] = {} + + csp = header_key == CSP::CONFIG_KEY ? self.current_csp : self.current_csp_report_only + unless csp == OPT_OUT user_agent = UserAgent.parse(user_agent) variation = CSP.ua_to_variation(user_agent) - self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent) + self.cached_headers[header_key][variation] = CSP.make_header(csp, user_agent) end end @@ -173,6 +181,10 @@ def current_csp @dynamic_csp || @csp end + def current_csp_report_only + @dynamic_csp_report_only || @csp_report_only + end + # Public: validates all configurations values. # # Raises various configuration errors if any invalid config is detected. @@ -181,6 +193,7 @@ def current_csp def validate_config! StrictTransportSecurity.validate_config!(@hsts) ContentSecurityPolicy.validate_config!(@csp) + ContentSecurityPolicy.validate_config!(@csp_report_only) ReferrerPolicy.validate_config!(@referrer_policy) XFrameOptions.validate_config!(@x_frame_options) XContentTypeOptions.validate_config!(@x_content_type_options) @@ -199,13 +212,36 @@ def secure_cookies=(secure_cookies) protected def csp=(new_csp) + unless new_csp == OPT_OUT + if new_csp[:report_only] + Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" + end + end + if self.dynamic_csp raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead." end - @csp = self.class.send(:deep_copy_if_hash, new_csp) end + def csp_report_only=(new_csp) + new_csp = self.class.send(:deep_copy_if_hash, new_csp) + unless new_csp == OPT_OUT + if new_csp[:report_only] == false + Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was supplied a config with report_only: false. Use #csp=" + end + + if new_csp[:report_only].nil? + new_csp[:report_only] = true + end + end + + if self.dynamic_csp_report_only + raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp_report_only= instead." + end + @csp_report_only = new_csp + end + def cookies=(cookies) @cookies = cookies end @@ -258,12 +294,16 @@ def cache_headers! # # Returns nothing def generate_csp_headers(headers) - unless @csp == OPT_OUT - headers[CSP::CONFIG_KEY] = {} - csp_config = self.current_csp + generate_csp_headers_for_config(headers, CSP::CONFIG_KEY, self.current_csp) + generate_csp_headers_for_config(headers, CSP::REPORT_ONLY_CONFIG_KEY, self.current_csp_report_only) + end + + def generate_csp_headers_for_config(headers, header_key, csp_config) + unless csp_config == OPT_OUT + headers[header_key] = {} CSP::VARIATIONS.each do |name, _| csp = CSP.make_header(csp_config, UserAgent.parse(name)) - headers[CSP::CONFIG_KEY][name] = csp.freeze + headers[header_key][name] = csp.freeze end end end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 58e0eb7b..1920e980 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -163,6 +163,7 @@ def self.included(base) }.freeze CONFIG_KEY = :csp + REPORT_ONLY_CONFIG_KEY = :csp_report_only STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) HTTP_SCHEME_REGEX = %r{\Ahttps?://} diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 2d8c432a..386129de 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -72,8 +72,11 @@ module SecureHeaders before(:all) do Configuration.default do |config| - config.csp[:script_src] = %w('self') - config.csp[:style_src] = %w('self') + config.csp = { + :default_src => %w('self'), + :script_src => %w('self'), + :style_src => %w('self') + } end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 4a07c107..dd79093c 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -29,8 +29,11 @@ module SecureHeaders describe "#header_hash_for" do it "allows you to opt out of individual headers via API" do - Configuration.default + Configuration.default do |config| + config.csp_report_only = { default_src: %w('self')} # no default value + end SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, CSP::REPORT_ONLY_CONFIG_KEY) SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy-Report-Only']).to be_nil @@ -151,14 +154,14 @@ module SecureHeaders expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end - it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do + it "dups global configuration just once when overriding n times and only calls idempotent_additions? once per header" do Configuration.default do |config| config.csp = { default_src: %w('self') } end - expect(CSP).to receive(:idempotent_additions?).once + expect(CSP).to receive(:idempotent_additions?).twice # before an override occurs, the env is empty expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil @@ -245,6 +248,81 @@ module SecureHeaders hash = SecureHeaders.header_hash_for(chrome_request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") end + + context "setting two headers" do + it "sets identical values when the configs are the same" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + config.csp_report_only = { + default_src: %w('self') + } + end + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + end + + it "sets different headers when the configs are different" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + config.csp_report_only = config.csp.merge({script_src: %w('self')}) + end + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self'") + end + + it "allows appending to the enforced policy" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + config.csp_report_only = config.csp + end + + SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + end + + it "allows appending to the report only policy" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + config.csp_report_only = config.csp + end + + SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") + end + + it "allows appending to both policies" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + config.csp_report_only = config.csp + end + + SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") + end + it "allows overriding the enforced policy" + it "allows overriding the report only policy" + it "allows overriding both policies" + end end end From 2e01a7fdca8c1075d738af595309d9374c49ec68 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 8 Jul 2016 12:13:48 -1000 Subject: [PATCH 267/636] wip --- lib/secure_headers.rb | 37 ++++++++++++++++++++++++----- lib/secure_headers/configuration.rb | 2 +- spec/lib/secure_headers_spec.rb | 7 +++++- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 940f30dd..002a823c 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -50,13 +50,31 @@ class << self # # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) - def override_content_security_policy_directives(request, additions) + def override_content_security_policy_directives(request, additions, target=:both) + raise_on_unknown_target(target) config = config_for(request) + + puts "before" + puts config.inspect + if config.current_csp == OPT_OUT config.dynamic_csp = {} end + if config.current_csp_report_only == OPT_OUT + config.dynamic_csp_report_only = {} + end + + if [:both, :enforced].include?(target) && config.current_csp != OPT_OUT + binding.pry + config.dynamic_csp = config.current_csp.merge(additions) + end + if [:both, :report_only].include?(target) && config.current_csp_report_only != OPT_OUT + binding.pry + config.dynamic_csp_report_only = config.current_csp_report_only.merge(additions) + end - config.dynamic_csp = config.current_csp.merge(additions) + puts "after" + puts config.inspect override_secure_headers_request_config(request, config) end @@ -67,11 +85,9 @@ def override_content_security_policy_directives(request, additions) # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) def append_content_security_policy_directives(request, additions, target=:both) - unless [:both, :enforced, :report_only].include?(target) - raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]" - end - + raise_on_unknown_target(target) config = config_for(request) + if [:both, :enforced].include?(target) && config.current_csp != OPT_OUT config.dynamic_csp = CSP.combine_policies(config.current_csp, additions) end @@ -118,6 +134,7 @@ def opt_out_of_all_protection(request) # in Rack middleware. def header_hash_for(request) config = config_for(request) + puts "final config#{ config.inspect}" unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp) config.rebuild_csp_header_cache!(request.user_agent, CSP::CONFIG_KEY) end @@ -126,6 +143,8 @@ def header_hash_for(request) config.rebuild_csp_header_cache!(request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) end + puts "final config with cache#{ config.inspect}" + use_cached_headers(config.cached_headers, request) end @@ -176,6 +195,12 @@ def config_for(request) end private + TARGETS = [:both, :enforced, :report_only] + def raise_on_unknown_target(target) + unless TARGETS.include?(target) + raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]" + end + end # Private: gets or creates a nonce for CSP. # diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 6329fc52..cae54af6 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -122,7 +122,7 @@ def deep_copy_if_hash(value) def initialize(&block) self.hpkp = OPT_OUT self.referrer_policy = OPT_OUT - self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG) + self.csp = OPT_OUT self.csp_report_only = OPT_OUT instance_eval &block if block_given? end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index dd79093c..f6e81936 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -207,9 +207,14 @@ module SecureHeaders end it "overrides non-existant directives" do - Configuration.default + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + end SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) hash = SecureHeaders.header_hash_for(request) + puts hash expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:") end From 7f581bccfdb4460201c6023e2f489b8f4468229e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 15 Jul 2016 15:41:06 -1000 Subject: [PATCH 268/636] get csp tests passing --- lib/secure_headers.rb | 38 +++++++++++++++++------------ lib/secure_headers/configuration.rb | 2 +- spec/lib/secure_headers_spec.rb | 9 ++++--- spec/spec_helper.rb | 2 +- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 002a823c..82358fe0 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -50,31 +50,24 @@ class << self # # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) - def override_content_security_policy_directives(request, additions, target=:both) - raise_on_unknown_target(target) - config = config_for(request) - - puts "before" - puts config.inspect + def override_content_security_policy_directives(request, additions, target = nil) + config, target = config_and_target(request, target) if config.current_csp == OPT_OUT config.dynamic_csp = {} end if config.current_csp_report_only == OPT_OUT - config.dynamic_csp_report_only = {} + config.dynamic_csp_report_only = { :report_only => true } end if [:both, :enforced].include?(target) && config.current_csp != OPT_OUT - binding.pry config.dynamic_csp = config.current_csp.merge(additions) end - if [:both, :report_only].include?(target) && config.current_csp_report_only != OPT_OUT - binding.pry + + if [:both, :report_only].include?(target) config.dynamic_csp_report_only = config.current_csp_report_only.merge(additions) end - puts "after" - puts config.inspect override_secure_headers_request_config(request, config) end @@ -84,13 +77,13 @@ def override_content_security_policy_directives(request, additions, target=:both # # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) - def append_content_security_policy_directives(request, additions, target=:both) - raise_on_unknown_target(target) - config = config_for(request) + def append_content_security_policy_directives(request, additions, target = nil) + config, target = config_and_target(request, target) if [:both, :enforced].include?(target) && config.current_csp != OPT_OUT config.dynamic_csp = CSP.combine_policies(config.current_csp, additions) end + if [:both, :report_only].include?(target) && config.current_csp_report_only != OPT_OUT config.dynamic_csp_report_only = CSP.combine_policies(config.current_csp_report_only, additions) end @@ -202,6 +195,21 @@ def raise_on_unknown_target(target) end end + def config_and_target(request, target) + config = config_for(request) + target = guess_target(config) unless target + raise_on_unknown_target(target) + [config, target] + end + + def guess_target(config) + if config.csp + :enforced + elsif config.csp_report_only + :report_only + end + end + # Private: gets or creates a nonce for CSP. # # Returns the nonce diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index cae54af6..2fc99fb7 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -84,6 +84,7 @@ def add_noop_configuration # Public: perform a basic deep dup. The shallow copy provided by dup/clone # can lead to modifying parent objects. def deep_copy(config) + return unless config config.each_with_object({}) do |(key, value), hash| hash[key] = if value.is_a?(Array) value.dup @@ -217,7 +218,6 @@ def csp=(new_csp) Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" end end - if self.dynamic_csp raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead." end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index f6e81936..3c99bec3 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -99,9 +99,9 @@ module SecureHeaders firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox])) # append an unsupported directive - SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash)) + SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)}, :enforced) # append a supported directive - SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self')) + SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')}, :enforced) hash = SecureHeaders.header_hash_for(firefox_request) @@ -178,7 +178,9 @@ module SecureHeaders end it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do - default_config = Configuration.default + default_config = Configuration.default do |config| + config.csp = { default_src: %w('none')}.freeze + end expect { default_config.csp = {} }.to raise_error(NoMethodError) # config is frozen @@ -215,6 +217,7 @@ module SecureHeaders SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) hash = SecureHeaders.header_hash_for(request) puts hash + expect(hash[CSP::REPORT_ONLY]).to be_nil expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:") end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9658eb89..637e12bb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,7 +25,6 @@ } def expect_default_values(hash) - expect(hash[SecureHeaders::CSP::HEADER_NAME]).to eq(SecureHeaders::CSP::DEFAULT_VALUE) expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) @@ -33,6 +32,7 @@ def expect_default_values(hash) expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE) expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil + expect(hash[SecureHeaders::CSP::HEADER_NAME]).to be(nil) end module SecureHeaders From 72a74ccd97190bba2eb6b355937d6c9a6072c5d1 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sun, 17 Jul 2016 22:47:11 -0600 Subject: [PATCH 269/636] wip' --- lib/secure_headers.rb | 7 +++++-- spec/lib/secure_headers/configuration_spec.rb | 4 +++- spec/lib/secure_headers/headers/policy_management_spec.rb | 7 ++++++- spec/lib/secure_headers_spec.rb | 3 +++ spec/spec_helper.rb | 2 +- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 82358fe0..361d1307 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -127,16 +127,19 @@ def opt_out_of_all_protection(request) # in Rack middleware. def header_hash_for(request) config = config_for(request) - puts "final config#{ config.inspect}" + puts "\nfinal config\n#{ config.inspect}" unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp) config.rebuild_csp_header_cache!(request.user_agent, CSP::CONFIG_KEY) end + puts "\nafter enforced changes\n#{ config.inspect}" + unless ContentSecurityPolicy.idempotent_additions?(config.csp_report_only, config.current_csp_report_only) config.rebuild_csp_header_cache!(request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) end - puts "final config with cache#{ config.inspect}" + puts "\nfinal config with cache\n#{ config.inspect}" + pp config.cached_headers use_cached_headers(config.cached_headers, request) end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 0f82c50e..f50a0225 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -4,7 +4,9 @@ module SecureHeaders describe Configuration do before(:each) do reset_config - Configuration.default + Configuration.default do |config| + config.csp = { default_src: %w(https:) } + end end it "has a default config" do diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 8ead3137..b91e1fc3 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -101,7 +101,12 @@ module SecureHeaders describe "#combine_policies" do it "combines the default-src value with the override if the directive was unconfigured" do - combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com)) + Configuration.default do |config| + config.csp = { + default_src: %w(https:) + } + end + combined_config = CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(CSP::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 3c99bec3..ccd5aa3e 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -105,6 +105,8 @@ module SecureHeaders hash = SecureHeaders.header_hash_for(firefox_request) + pp hash + # child-src is translated to frame-src expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'") end @@ -188,6 +190,7 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) new_config = SecureHeaders.config_for(request) + $debug = true expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError) expect do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 637e12bb..d8603f26 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,7 @@ } def expect_default_values(hash) + expect(hash[SecureHeaders::CSP::HEADER_NAME]).to be(nil) expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) @@ -32,7 +33,6 @@ def expect_default_values(hash) expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE) expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil - expect(hash[SecureHeaders::CSP::HEADER_NAME]).to be(nil) end module SecureHeaders From 4d311143bbf2cde1ed6f642e7748ac6bc07fc655 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sun, 17 Jul 2016 22:53:39 -0600 Subject: [PATCH 270/636] bump to 3.4.0 and include child/frame-src handling --- CHANGELOG.md | 14 ++++++++++++++ secure_headers.gemspec | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1abfc94f..41ddc15f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 3.4.0 the frame-src/child-src transition for Firefox. + +Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here: + +```ruby +if supported_directives.include?(:child_src) + @config[:child_src] = @config[:child_src] || @config[:frame_src] +else + @config[:frame_src] = @config[:frame_src] || @config[:child_src] +end +``` + +Also, @koenpunt noticed that we were [loading view helpers](https://github.com/twitter/secureheaders/pull/272) in a way that Rails 5 did not like. + ## 3.3.2 minor fix to silence warnings when using rake [@dankohn](https://github.com/twitter/secureheaders/issues/257) was seeing "already initialized" errors in his output. This change conditionally defines the constants. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 96bf0651..b1055ce1 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.3.2" + gem.version = "3.4.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 76e60a6aaa2d929d2493a6c5c7c949b4c323e279 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Sun, 17 Jul 2016 22:56:56 -0600 Subject: [PATCH 271/636] add docs about child/frame-src handling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8402844..832ee8f9 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ SecureHeaders::Configuration.default do |config| default_src: %w(https: 'self'), base_uri: %w('self'), block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ - child_src: %w('self'), + child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. connect_src: %w(wss:), font_src: %w('self' data:), form_action: %w('self' github.com), From 48f92000870478d1156e80a2e24241b8b40a66f2 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 26 Jul 2016 13:12:33 -1000 Subject: [PATCH 272/636] initial work for making csp config an actual class --- .../headers/content_security_policy.rb | 41 +++++++++++-------- .../headers/policy_management.rb | 5 +++ spec/lib/secure_headers/configuration_spec.rb | 4 +- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index efee3428..c92469d0 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -1,8 +1,8 @@ require_relative 'policy_management' +require_relative 'content_security_policy_config' require 'useragent' module SecureHeaders - class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicy include PolicyManagement @@ -10,17 +10,22 @@ class ContentSecurityPolicy VERSION_46 = ::UserAgent::Version.new("46") def initialize(config = nil, user_agent = OTHER) - @config = Configuration.send(:deep_copy, config || DEFAULT_CONFIG) + @config = if config.is_a?(Hash) + ContentSecurityPolicyConfig.new(Configuration.send(:deep_copy, config || DEFAULT_CONFIG)) + else + config + end + @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base) user_agent else UserAgent.parse(user_agent) end normalize_child_frame_src - @report_only = @config[:report_only] - @preserve_schemes = @config[:preserve_schemes] - @script_nonce = @config[:script_nonce] - @style_nonce = @config[:style_nonce] + @report_only = @config.report_only + @preserve_schemes = @config.preserve_schemes + @script_nonce = @config.script_nonce + @style_nonce = @config.style_nonce end ## @@ -49,16 +54,16 @@ def value # frame-src is deprecated, child-src is being implemented. They are # very similar and in most cases, the same value can be used for both. def normalize_child_frame_src - if @config[:frame_src] && @config[:child_src] && @config[:frame_src] != @config[:child_src] + if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.") - elsif @config[:frame_src] - Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}.") + elsif @config.frame_src + Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config.frame_src}.") end if supported_directives.include?(:child_src) - @config[:child_src] = @config[:child_src] || @config[:frame_src] + @config.child_src = @config.child_src || @config.frame_src else - @config[:frame_src] = @config[:frame_src] || @config[:child_src] + @config.frame_src = @config.frame_src || @config.child_src end end @@ -73,9 +78,9 @@ def build_value directives.map do |directive_name| case DIRECTIVE_VALUE_TYPES[directive_name] when :boolean - symbol_to_hyphen_case(directive_name) if @config[directive_name] + symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) when :string - [symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ") + [symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ") else build_directive(directive_name) end @@ -88,9 +93,9 @@ def build_value # # Returns a string representing a directive. def build_directive(directive) - return if @config[directive].nil? + return if @config.directive_value(directive).nil? - source_list = @config[directive].compact + source_list = @config.directive_value(directive).compact return if source_list.empty? normalized_source_list = minify_source_list(directive, source_list) @@ -170,9 +175,11 @@ def append_nonce(source_list, nonce) # Private: return the list of directives that are supported by the user agent, # starting with default-src and ending with report-uri. def directives - [DEFAULT_SRC, + [ + DEFAULT_SRC, BODY_DIRECTIVES.select { |key| supported_directives.include?(key) }, - REPORT_URI].flatten.select { |directive| @config.key?(directive) } + REPORT_URI + ].flatten.select { |directive| @config.directive_value(directive) } end # Private: Remove scheme from source expressions. diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 1920e980..9331de48 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -180,6 +180,11 @@ def self.included(base) :preserve_schemes ].freeze + NONCES = [ + :script_nonce, + :style_nonce + ].freeze + module ClassMethods # Public: generate a header name, value array that is user-agent-aware. # diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index f50a0225..0f82c50e 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -4,9 +4,7 @@ module SecureHeaders describe Configuration do before(:each) do reset_config - Configuration.default do |config| - config.csp = { default_src: %w(https:) } - end + Configuration.default end it "has a default config" do From e9fa410a7ba14b2607530ec84b5fba8f2b5633ca Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 27 Jul 2016 12:19:03 -1000 Subject: [PATCH 273/636] wip --- lib/secure_headers.rb | 5 +- .../headers/content_security_policy_config.rb | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 lib/secure_headers/headers/content_security_policy_config.rb diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 361d1307..94477af8 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -18,7 +18,10 @@ # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT # or ":optout_of_protection" as a config value to disable a given header module SecureHeaders - OPT_OUT = :opt_out_of_protection + class NoOpHeaderConfig + end + + OPT_OUT = NoOpHeaderConfig.new SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze HTTPS = "https".freeze diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb new file mode 100644 index 00000000..981469d8 --- /dev/null +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -0,0 +1,67 @@ +module SecureHeaders + module DynamicConfig + def self.included(base) + base.send(:attr_reader, *base.attrs) + base.attrs.each do |attr| + base.send(:define_method, "#{attr}=") do |value| + if self.class.attrs.include?(attr) + if PolicyManagement::DIRECTIVE_VALUE_TYPES[k] == :source_list + instance_variable_set("@{k}=", value.dup) + self.send("#{attr}=", value.dup) + else + self.send("#{attr}=", value) + end + else + raise "Unknown config directive: #{attr}" + end + + @modified = true + end + end + end + + def initialize(hash) + hash.keys.map do |k| + next unless hash[k] + if self.class.attrs.include?(k) + if PolicyManagement::DIRECTIVE_VALUE_TYPES[k] == :source_list + self.send("#{k}=", hash[k].dup) + else + self.send("#{k}=", hash[k]) + end + else + binding.pry + raise "Unknown config directive: #{k}" + end + end + end + + def update_directive(directive, value) + if self.class.attrs.include?(directive) + if PolicyManagement::DIRECTIVE_VALUE_TYPES[k] == :source_list + instance_variable_set("@{k}=", hash[k].dup) + else + self.send("#{k}=", hash[k]) + end + end + + + end + + def directive_value(directive) + if self.class.attrs.include?(directive) + self.send(directive) + end + end + end + + class ContentSecurityPolicyConfigError < StandardError; end + class ContentSecurityPolicyConfig + def self.attrs + PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES + end + + include DynamicConfig + alias_method :report_only?, :report_only + end +end From 387f7a69a4e77b15dace6ff8010c0585091fa1e3 Mon Sep 17 00:00:00 2001 From: Lalith Rallabhandi Date: Mon, 1 Aug 2016 18:42:40 -0400 Subject: [PATCH 274/636] Minor change in domain --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 832ee8f9..5458c9d9 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ end class MyController < ApplicationController def index - # Produces default-src 'self'; script-src example.org otherdomain.org + # Produces default-src 'self'; script-src example.org otherdomain.com use_secure_headers_override(:script_from_otherdomain_com) end From 27f27439de8b7e12c6f15111c825bc66395fd0d3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 8 Aug 2016 13:21:15 -1000 Subject: [PATCH 275/636] base spec is passing --- lib/secure_headers.rb | 7 ++ lib/secure_headers/configuration.rb | 8 +-- .../headers/content_security_policy.rb | 41 +++++++---- .../headers/content_security_policy_config.rb | 68 +++++++++++-------- .../headers/policy_management.rb | 6 +- spec/lib/secure_headers/configuration_spec.rb | 4 +- spec/spec_helper.rb | 2 +- 7 files changed, 84 insertions(+), 52 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 94477af8..347ae28d 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -19,6 +19,13 @@ # or ":optout_of_protection" as a config value to disable a given header module SecureHeaders class NoOpHeaderConfig + def boom + raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead." + end + + alias_method :[], :boom + alias_method :[]=, :boom + alias_method :keys, :boom end OPT_OUT = NoOpHeaderConfig.new diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 2fc99fb7..f8e6a36e 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -123,7 +123,7 @@ def deep_copy_if_hash(value) def initialize(&block) self.hpkp = OPT_OUT self.referrer_policy = OPT_OUT - self.csp = OPT_OUT + self.csp = ContentSecurityPolicyConfig::DEFAULT self.csp_report_only = OPT_OUT instance_eval &block if block_given? end @@ -134,10 +134,8 @@ def initialize(&block) def dup copy = self.class.new copy.cookies = @cookies - copy.csp = self.class.send(:deep_copy_if_hash, @csp) - copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp) - copy.csp_report_only = self.class.send(:deep_copy_if_hash, @csp_report_only) - copy.dynamic_csp_report_only = self.class.send(:deep_copy_if_hash, @dynamic_csp_report_only) + copy.csp = @csp.dup + copy.csp_report_only = @csp_report_only.dup copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) copy.x_content_type_options = @x_content_type_options copy.hsts = @hsts diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index c92469d0..efb60bea 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -11,7 +11,11 @@ class ContentSecurityPolicy def initialize(config = nil, user_agent = OTHER) @config = if config.is_a?(Hash) - ContentSecurityPolicyConfig.new(Configuration.send(:deep_copy, config || DEFAULT_CONFIG)) + if config[:report_only] + ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG) + else + ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG) + end else config end @@ -21,8 +25,7 @@ def initialize(config = nil, user_agent = OTHER) else UserAgent.parse(user_agent) end - normalize_child_frame_src - @report_only = @config.report_only + @frame_src = normalize_child_frame_src @preserve_schemes = @config.preserve_schemes @script_nonce = @config.script_nonce @style_nonce = @config.style_nonce @@ -32,7 +35,7 @@ def initialize(config = nil, user_agent = OTHER) # Returns the name to use for the header. Either "Content-Security-Policy" or # "Content-Security-Policy-Report-Only" def name - if @report_only + if @config.report_only? REPORT_ONLY else HEADER_NAME @@ -61,9 +64,9 @@ def normalize_child_frame_src end if supported_directives.include?(:child_src) - @config.child_src = @config.child_src || @config.frame_src + @config.child_src || @config.frame_src else - @config.frame_src = @config.frame_src || @config.child_src + @config.frame_src || @config.child_src end end @@ -76,8 +79,10 @@ def normalize_child_frame_src # Returns a content security policy header value. def build_value directives.map do |directive_name| + puts directive_name case DIRECTIVE_VALUE_TYPES[directive_name] when :boolean + puts " #{@config.directive_value(directive_name)}" symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) when :string [symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ") @@ -93,11 +98,20 @@ def build_value # # Returns a string representing a directive. def build_directive(directive) - return if @config.directive_value(directive).nil? - - source_list = @config.directive_value(directive).compact - return if source_list.empty? - + source_list = case directive + when :child_src + if supported_directives.include?(:child_src) + @frame_src + end + when :frame_src + unless supported_directives.include?(:child_src) + @frame_src + end + else + @config.directive_value(directive) + end + puts " #{directive}=#{source_list}" + return unless source_list && source_list.any? normalized_source_list = minify_source_list(directive, source_list) [symbol_to_hyphen_case(directive), normalized_source_list].join(" ") end @@ -106,6 +120,7 @@ def build_directive(directive) # If a directive contains 'none' but has other values, 'none' is ommitted. # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression) def minify_source_list(directive, source_list) + source_list.compact! if source_list.include?(STAR) keep_wildcard_sources(source_list) else @@ -115,7 +130,7 @@ def minify_source_list(directive, source_list) unless directive == REPORT_URI || @preserve_schemes strip_source_schemes!(source_list) end - dedup_source_list(source_list).join(" ") + dedup_source_list(source_list) end end @@ -179,7 +194,7 @@ def directives DEFAULT_SRC, BODY_DIRECTIVES.select { |key| supported_directives.include?(key) }, REPORT_URI - ].flatten.select { |directive| @config.directive_value(directive) } + ].flatten end # Private: Remove scheme from source expressions. diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 981469d8..4e880986 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -5,47 +5,29 @@ def self.included(base) base.attrs.each do |attr| base.send(:define_method, "#{attr}=") do |value| if self.class.attrs.include?(attr) - if PolicyManagement::DIRECTIVE_VALUE_TYPES[k] == :source_list - instance_variable_set("@{k}=", value.dup) - self.send("#{attr}=", value.dup) - else - self.send("#{attr}=", value) + value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list + prev_value = self.instance_variable_get("@#{attr}") + self.instance_variable_set("@#{attr}", value) + if prev_value != self.instance_variable_get("@#{attr}") + @modified = true end else - raise "Unknown config directive: #{attr}" + raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}" end - - @modified = true end end end def initialize(hash) - hash.keys.map do |k| - next unless hash[k] - if self.class.attrs.include?(k) - if PolicyManagement::DIRECTIVE_VALUE_TYPES[k] == :source_list - self.send("#{k}=", hash[k].dup) - else - self.send("#{k}=", hash[k]) - end - else - binding.pry - raise "Unknown config directive: #{k}" - end + hash.keys.reject { |k| hash[k].nil? }.map do |k| + self.send("#{k}=", hash[k]) end + + @modified = false end def update_directive(directive, value) - if self.class.attrs.include?(directive) - if PolicyManagement::DIRECTIVE_VALUE_TYPES[k] == :source_list - instance_variable_set("@{k}=", hash[k].dup) - else - self.send("#{k}=", hash[k]) - end - end - - + self.send("#{k}=", value) end def directive_value(directive) @@ -53,6 +35,13 @@ def directive_value(directive) self.send(directive) end end + + def modified? + @modified + end + + alias_method :[], :directive_value + alias_method :[]=, :update_directive end class ContentSecurityPolicyConfigError < StandardError; end @@ -62,6 +51,25 @@ def self.attrs end include DynamicConfig - alias_method :report_only?, :report_only + + # based on what was suggested in https://github.com/rails/rails/pull/24961/files + DEFAULT = { + default_src: %w('self' https:), + font_src: %w('self' https: data:), + img_src: %w('self' https: data:), + object_src: %w('none'), + script_src: %w(https:), + style_src: %w('self' https: 'unsafe-inline') + } + + def report_only? + false + end + end + + class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig + def report_only? + true + end end end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 9331de48..e8a53796 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -156,7 +156,7 @@ def self.included(base) PLUGIN_TYPES => :source_list, REFLECTED_XSS => :string, REPORT_URI => :source_list, - SANDBOX => :string, + SANDBOX => :source_list, SCRIPT_SRC => :source_list, STYLE_SRC => :source_list, UPGRADE_INSECURE_REQUESTS => :boolean @@ -202,7 +202,9 @@ def make_header(config, user_agent) def validate_config!(config) return if config.nil? || config == OPT_OUT raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src] - config.each do |key, value| + ContentSecurityPolicyConfig.attrs.each do |key| + value = config.directive_value(key) + next unless value if META_CONFIGS.include?(key) raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil? else diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 0f82c50e..b7addd4b 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -4,7 +4,9 @@ module SecureHeaders describe Configuration do before(:each) do reset_config - Configuration.default + Configuration.default do |config| + config.csp = ContentSecurityPolicyConfig.new(:default_src => %w(https:)) + end end it "has a default config" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d8603f26..b8e31bda 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,7 +25,7 @@ } def expect_default_values(hash) - expect(hash[SecureHeaders::CSP::HEADER_NAME]).to be(nil) + expect(hash[SecureHeaders::CSP::HEADER_NAME]).to be("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'") expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) From dbea8c6bf663de8bf5c91f17fca3899f67efbea8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 8 Aug 2016 14:03:14 -1000 Subject: [PATCH 276/636] specs for the configuration class now pass --- lib/secure_headers.rb | 15 ++++- lib/secure_headers/configuration.rb | 67 +++++++------------ .../headers/content_security_policy.rb | 2 + .../headers/content_security_policy_config.rb | 24 ++++++- .../headers/policy_management.rb | 4 +- spec/lib/secure_headers/configuration_spec.rb | 14 ++-- .../headers/policy_management_spec.rb | 32 ++++----- spec/spec_helper.rb | 2 +- 8 files changed, 89 insertions(+), 71 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 347ae28d..de422b4a 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -14,21 +14,32 @@ require "secure_headers/railtie" require "secure_headers/view_helper" require "useragent" +require "singleton" # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT # or ":optout_of_protection" as a config value to disable a given header module SecureHeaders class NoOpHeaderConfig - def boom + include Singleton + + def boom(arg = nil) raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead." end + def to_h + {} + end + + def dup + self.class.instance + end + alias_method :[], :boom alias_method :[]=, :boom alias_method :keys, :boom end - OPT_OUT = NoOpHeaderConfig.new + OPT_OUT = NoOpHeaderConfig.instance SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze HTTPS = "https".freeze diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index f8e6a36e..c667b90c 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -74,8 +74,6 @@ def add_noop_configuration ALL_HEADER_CLASSES.each do |klass| config.send("#{klass::CONFIG_KEY}=", OPT_OUT) end - config.dynamic_csp = OPT_OUT - config.dynamic_csp_report_only = OPT_OUT end add_configuration(NOOP_CONFIGURATION, noop_config) @@ -105,8 +103,6 @@ def deep_copy_if_hash(value) end end - attr_accessor :dynamic_csp, :dynamic_csp_report_only - attr_writer :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, :referrer_policy @@ -134,8 +130,8 @@ def initialize(&block) def dup copy = self.class.new copy.cookies = @cookies - copy.csp = @csp.dup - copy.csp_report_only = @csp_report_only.dup + copy.csp = @csp.dup if @csp + copy.csp_report_only = @csp_report_only.dup if @csp_report_only copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) copy.x_content_type_options = @x_content_type_options copy.hsts = @hsts @@ -151,11 +147,6 @@ def dup def opt_out(header) send("#{header}=", OPT_OUT) - if header == CSP::CONFIG_KEY - self.dynamic_csp = OPT_OUT - elsif header == CSP::REPORT_ONLY_CONFIG_KEY - self.dynamic_csp_report_only = OPT_OUT - end self.cached_headers.delete(header) end @@ -168,7 +159,7 @@ def update_x_frame_options(value) def rebuild_csp_header_cache!(user_agent, header_key) self.cached_headers[header_key] = {} - csp = header_key == CSP::CONFIG_KEY ? self.current_csp : self.current_csp_report_only + csp = header_key == CSP::CONFIG_KEY ? self.csp : self.csp_report_only unless csp == OPT_OUT user_agent = UserAgent.parse(user_agent) variation = CSP.ua_to_variation(user_agent) @@ -176,14 +167,6 @@ def rebuild_csp_header_cache!(user_agent, header_key) end end - def current_csp - @dynamic_csp || @csp - end - - def current_csp_report_only - @dynamic_csp_report_only || @csp_report_only - end - # Public: validates all configurations values. # # Raises various configuration errors if any invalid config is detected. @@ -211,33 +194,35 @@ def secure_cookies=(secure_cookies) protected def csp=(new_csp) - unless new_csp == OPT_OUT - if new_csp[:report_only] - Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" + if new_csp == OPT_OUT + @csp = OPT_OUT + else + if new_csp.is_a?(ContentSecurityPolicyConfig) + @csp = new_csp + else + if new_csp[:report_only] + Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" + self.csp_report_only = new_csp + else + @csp = ContentSecurityPolicyConfig.new(new_csp) + end end end - if self.dynamic_csp - raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead." - end - @csp = self.class.send(:deep_copy_if_hash, new_csp) end def csp_report_only=(new_csp) - new_csp = self.class.send(:deep_copy_if_hash, new_csp) - unless new_csp == OPT_OUT - if new_csp[:report_only] == false - Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was supplied a config with report_only: false. Use #csp=" - end - - if new_csp[:report_only].nil? - new_csp[:report_only] = true + if new_csp == OPT_OUT + @csp_report_only = OPT_OUT + else + if new_csp.is_a?(Hash) + new_csp = ContentSecurityPolicyReportOnlyConfig.new(new_csp) + if new_csp[:report_only] == false + Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was supplied a config with report_only: false. Use #csp=" + end end - end - if self.dynamic_csp_report_only - raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp_report_only= instead." + @csp_report_only = new_csp end - @csp_report_only = new_csp end def cookies=(cookies) @@ -292,8 +277,8 @@ def cache_headers! # # Returns nothing def generate_csp_headers(headers) - generate_csp_headers_for_config(headers, CSP::CONFIG_KEY, self.current_csp) - generate_csp_headers_for_config(headers, CSP::REPORT_ONLY_CONFIG_KEY, self.current_csp_report_only) + generate_csp_headers_for_config(headers, CSP::CONFIG_KEY, self.csp) + generate_csp_headers_for_config(headers, CSP::REPORT_ONLY_CONFIG_KEY, self.csp_report_only) end def generate_csp_headers_for_config(headers, header_key, csp_config) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index efb60bea..c172150f 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -16,6 +16,8 @@ def initialize(config = nil, user_agent = OTHER) else ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG) end + elsif config.nil? + ContentSecurityPolicyConfig.new(DEFAULT_CONFIG) else config end diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 4e880986..daf6d4e6 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -20,7 +20,11 @@ def self.included(base) def initialize(hash) hash.keys.reject { |k| hash[k].nil? }.map do |k| - self.send("#{k}=", hash[k]) + if self.class.attrs.include?(k) + self.send("#{k}=", hash[k]) + else + raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}" + end end @modified = false @@ -40,6 +44,24 @@ def modified? @modified end + def merge(new_hash) + self.class.new(CSP.combine_policies(self.to_h, new_hash)) + end + + def to_h + self.class.attrs.each_with_object({}) do |key, hash| + hash[key] = self.send(key) + end.reject { |_, v| v.nil? } + end + + def dup + self.class.new(self.to_h) + end + + def ==(o) + self.class == o.class && self.to_h == o.to_h + end + alias_method :[], :directive_value alias_method :[]=, :update_directive end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index e8a53796..e9921929 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -201,7 +201,7 @@ def make_header(config, user_agent) # script_src => h*t*t*p: will not raise an exception) def validate_config!(config) return if config.nil? || config == OPT_OUT - raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src] + raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src) ContentSecurityPolicyConfig.attrs.each do |key| value = config.directive_value(key) next unless value @@ -239,7 +239,7 @@ def idempotent_additions?(config, additions) # 3. if a value in additions does exist in the original config, the two # values are joined. def combine_policies(original, additions) - if original == OPT_OUT + if original == {} raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index b7addd4b..40b285e5 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -4,9 +4,7 @@ module SecureHeaders describe Configuration do before(:each) do reset_config - Configuration.default do |config| - config.csp = ContentSecurityPolicyConfig.new(:default_src => %w(https:)) - end + Configuration.default end it "has a default config" do @@ -38,8 +36,8 @@ module SecureHeaders config = Configuration.get(:test_override) noop = Configuration.get(Configuration::NOOP_CONFIGURATION) - [:csp, :dynamic_csp, :cookies].each do |key| - expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}." + [:csp, :csp_report_only, :cookies].each do |key| + expect(config.send(key)).to eq(noop.send(key)) end end @@ -67,7 +65,7 @@ module SecureHeaders default = Configuration.get override = Configuration.get(:override) - expect(override.csp).not_to eq(default.csp) + expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src)) end it "allows you to override an override" do @@ -80,9 +78,9 @@ module SecureHeaders end original_override = Configuration.get(:override) - expect(original_override.csp).to eq(default_src: %w('self')) + expect(original_override.csp.to_h).to eq(default_src: %w('self')) override_config = Configuration.get(:second_override) - expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org)) + expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org)) end it "deprecates the secure_cookies configuration" do diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index b91e1fc3..e92dbdd5 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -40,61 +40,61 @@ module SecureHeaders report_uri: %w(https://example.com/uri-directive) } - CSP.validate_config!(config) + CSP.validate_config!(ContentSecurityPolicyConfig.new(config)) end it "requires a :default_src value" do expect do - CSP.validate_config!(script_src: %('self')) + CSP.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :report_only to be a truthy value" do expect do - CSP.validate_config!(default_opts.merge(report_only: "steve")) + CSP.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :preserve_schemes to be a truthy value" do expect do - CSP.validate_config!(default_opts.merge(preserve_schemes: "steve")) + CSP.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :block_all_mixed_content to be a boolean value" do expect do - CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve")) + CSP.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :upgrade_insecure_requests to be a boolean value" do expect do - CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve")) + CSP.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires all source lists to be an array of strings" do expect do - CSP.validate_config!(default_src: "steve") + CSP.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve")) end.to raise_error(ContentSecurityPolicyConfigError) end it "allows nil values" do expect do - CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil]) + CSP.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil])) end.to_not raise_error end it "rejects unknown directives / config" do expect do - CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve") + CSP.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve")) end.to raise_error(ContentSecurityPolicyConfigError) end # this is mostly to ensure people don't use the antiquated shorthands common in other configs it "performs light validation on source lists" do expect do - CSP.validate_config!(default_src: %w(self none inline eval)) + CSP.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval))) end.to raise_error(ContentSecurityPolicyConfigError) end end @@ -106,7 +106,7 @@ module SecureHeaders default_src: %w(https:) } end - combined_config = CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com)) + combined_config = CSP.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(CSP::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") @@ -120,7 +120,7 @@ module SecureHeaders }.freeze end report_uri = "https://report-uri.io/asdf" - combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri]) + combined_config = CSP.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri]) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.value).to include("report-uri #{report_uri}") end @@ -135,7 +135,7 @@ module SecureHeaders non_default_source_additions = CSP::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end - combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions) + combined_config = CSP.combine_policies(Configuration.get.csp.to_h, non_default_source_additions) CSP::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) @@ -151,7 +151,7 @@ module SecureHeaders report_only: false } end - combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true) + combined_config = CSP.combine_policies(Configuration.get.csp.to_h, report_only: true) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.name).to eq(CSP::REPORT_ONLY) end @@ -163,7 +163,7 @@ module SecureHeaders block_all_mixed_content: false } end - combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true) + combined_config = CSP.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to eq("default-src https:; block-all-mixed-content") end @@ -173,7 +173,7 @@ module SecureHeaders config.csp = OPT_OUT end expect do - CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com)) + CSP.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) end.to raise_error(ContentSecurityPolicyConfigError) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b8e31bda..23fc8f62 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,7 +25,7 @@ } def expect_default_values(hash) - expect(hash[SecureHeaders::CSP::HEADER_NAME]).to be("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'") + expect(hash[SecureHeaders::CSP::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'") expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) From 893d6e9aa9a4177c24c36b87dcfac2d7c4ad8ef7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 8 Aug 2016 15:30:08 -1000 Subject: [PATCH 277/636] get append/override working --- lib/secure_headers.rb | 32 +++++++------ lib/secure_headers/configuration.rb | 12 +++-- .../headers/content_security_policy_config.rb | 33 +++++++++---- spec/lib/secure_headers_spec.rb | 46 +------------------ 4 files changed, 48 insertions(+), 75 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index de422b4a..18da587f 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -74,19 +74,19 @@ class << self def override_content_security_policy_directives(request, additions, target = nil) config, target = config_and_target(request, target) - if config.current_csp == OPT_OUT - config.dynamic_csp = {} + if config.csp == OPT_OUT + config.csp = ContentSecurityPolicyConfig.new({}) end - if config.current_csp_report_only == OPT_OUT - config.dynamic_csp_report_only = { :report_only => true } + if config.csp_report_only == OPT_OUT + config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({}) end - if [:both, :enforced].include?(target) && config.current_csp != OPT_OUT - config.dynamic_csp = config.current_csp.merge(additions) + if [:both, :enforced].include?(target) + config.csp.merge!(additions) end if [:both, :report_only].include?(target) - config.dynamic_csp_report_only = config.current_csp_report_only.merge(additions) + config.csp_report_only.merge!(additions) end override_secure_headers_request_config(request, config) @@ -101,12 +101,12 @@ def override_content_security_policy_directives(request, additions, target = nil def append_content_security_policy_directives(request, additions, target = nil) config, target = config_and_target(request, target) - if [:both, :enforced].include?(target) && config.current_csp != OPT_OUT - config.dynamic_csp = CSP.combine_policies(config.current_csp, additions) + if [:both, :enforced].include?(target) && config.csp != OPT_OUT + config.csp.append(additions) end - if [:both, :report_only].include?(target) && config.current_csp_report_only != OPT_OUT - config.dynamic_csp_report_only = CSP.combine_policies(config.current_csp_report_only, additions) + if [:both, :report_only].include?(target) && config.csp_report_only != OPT_OUT + config.csp_report_only.append(additions) end override_secure_headers_request_config(request, config) @@ -149,13 +149,13 @@ def opt_out_of_all_protection(request) def header_hash_for(request) config = config_for(request) puts "\nfinal config\n#{ config.inspect}" - unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp) + if config.csp != OPT_OUT && config.csp.modified? config.rebuild_csp_header_cache!(request.user_agent, CSP::CONFIG_KEY) end puts "\nafter enforced changes\n#{ config.inspect}" - unless ContentSecurityPolicy.idempotent_additions?(config.csp_report_only, config.current_csp_report_only) + if config.csp_report_only != OPT_OUT && config.csp_report_only.modified? config.rebuild_csp_header_cache!(request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) end @@ -227,10 +227,12 @@ def config_and_target(request, target) end def guess_target(config) - if config.csp + if config.csp != OPT_OUT :enforced - elsif config.csp_report_only + elsif config.csp_report_only != OPT_OUT :report_only + else + :both end end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index c667b90c..b85d9e69 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -191,14 +191,12 @@ def secure_cookies=(secure_cookies) @cookies = (@cookies || {}).merge(secure: secure_cookies) end - protected - def csp=(new_csp) if new_csp == OPT_OUT @csp = OPT_OUT else if new_csp.is_a?(ContentSecurityPolicyConfig) - @csp = new_csp + @csp = new_csp.dup else if new_csp[:report_only] Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" @@ -214,17 +212,21 @@ def csp_report_only=(new_csp) if new_csp == OPT_OUT @csp_report_only = OPT_OUT else - if new_csp.is_a?(Hash) - new_csp = ContentSecurityPolicyReportOnlyConfig.new(new_csp) + new_csp = if new_csp.is_a?(ContentSecurityPolicyConfig) + ContentSecurityPolicyReportOnlyConfig.new(new_csp.to_h) + elsif new_csp.is_a?(Hash) if new_csp[:report_only] == false Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was supplied a config with report_only: false. Use #csp=" end + ContentSecurityPolicyReportOnlyConfig.new(new_csp) end @csp_report_only = new_csp end end + protected + def cookies=(cookies) @cookies = cookies end diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index daf6d4e6..abf22508 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -1,6 +1,7 @@ module SecureHeaders module DynamicConfig def self.included(base) + base.send(:attr_writer, :modified) base.send(:attr_reader, *base.attrs) base.attrs.each do |attr| base.send(:define_method, "#{attr}=") do |value| @@ -19,19 +20,12 @@ def self.included(base) end def initialize(hash) - hash.keys.reject { |k| hash[k].nil? }.map do |k| - if self.class.attrs.include?(k) - self.send("#{k}=", hash[k]) - else - raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}" - end - end - + from_hash(hash) @modified = false end def update_directive(directive, value) - self.send("#{k}=", value) + self.send("#{directive}=", value) end def directive_value(directive) @@ -45,7 +39,15 @@ def modified? end def merge(new_hash) - self.class.new(CSP.combine_policies(self.to_h, new_hash)) + CSP.combine_policies(self.to_h, new_hash) + end + + def merge!(new_hash) + from_hash(new_hash) + end + + def append(new_hash) + from_hash(CSP.combine_policies(self.to_h, new_hash)) end def to_h @@ -64,6 +66,17 @@ def ==(o) alias_method :[], :directive_value alias_method :[]=, :update_directive + + private + def from_hash(hash) + hash.keys.reject { |k| hash[k].nil? }.map do |k| + if self.class.attrs.include?(k) + self.send("#{k}=", hash[k]) + else + raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}" + end + end + end end class ContentSecurityPolicyConfigError < StandardError; end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index ccd5aa3e..3b6da48b 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -156,50 +156,6 @@ module SecureHeaders expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end - it "dups global configuration just once when overriding n times and only calls idempotent_additions? once per header" do - Configuration.default do |config| - config.csp = { - default_src: %w('self') - } - end - - expect(CSP).to receive(:idempotent_additions?).twice - - # before an override occurs, the env is empty - expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil - - SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) - new_config = SecureHeaders.config_for(request) - expect(new_config).to_not be(Configuration.get) - - SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com)) - current_config = SecureHeaders.config_for(request) - expect(current_config).to be(new_config) - - SecureHeaders.header_hash_for(request) - end - - it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do - default_config = Configuration.default do |config| - config.csp = { default_src: %w('none')}.freeze - end - expect { default_config.csp = {} }.to raise_error(NoMethodError) - - # config is frozen - expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError) - - SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) - new_config = SecureHeaders.config_for(request) - $debug = true - expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError) - - expect do - new_config.instance_eval do - new_config.csp = {} - end - end.to raise_error(Configuration::IllegalPolicyModificationError) - end - it "overrides individual directives" do Configuration.default do |config| config.csp = { @@ -214,7 +170,7 @@ module SecureHeaders it "overrides non-existant directives" do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w(https:) } end SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) From 40313cef1f77b71e76c57e0bed971bcfe35dd40c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 8 Aug 2016 15:43:44 -1000 Subject: [PATCH 278/636] add tests for overriding with two headers --- lib/secure_headers.rb | 6 -- .../headers/content_security_policy.rb | 3 - spec/lib/secure_headers_spec.rb | 55 ++++++++++--------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 18da587f..b705e170 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -148,20 +148,14 @@ def opt_out_of_all_protection(request) # in Rack middleware. def header_hash_for(request) config = config_for(request) - puts "\nfinal config\n#{ config.inspect}" if config.csp != OPT_OUT && config.csp.modified? config.rebuild_csp_header_cache!(request.user_agent, CSP::CONFIG_KEY) end - puts "\nafter enforced changes\n#{ config.inspect}" - if config.csp_report_only != OPT_OUT && config.csp_report_only.modified? config.rebuild_csp_header_cache!(request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) end - puts "\nfinal config with cache\n#{ config.inspect}" - pp config.cached_headers - use_cached_headers(config.cached_headers, request) end diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index c172150f..f55fd620 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -81,10 +81,8 @@ def normalize_child_frame_src # Returns a content security policy header value. def build_value directives.map do |directive_name| - puts directive_name case DIRECTIVE_VALUE_TYPES[directive_name] when :boolean - puts " #{@config.directive_value(directive_name)}" symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) when :string [symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ") @@ -112,7 +110,6 @@ def build_directive(directive) else @config.directive_value(directive) end - puts " #{directive}=#{source_list}" return unless source_list && source_list.any? normalized_source_list = minify_source_list(directive, source_list) [symbol_to_hyphen_case(directive), normalized_source_list].join(" ") diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 3b6da48b..8b36ec76 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -175,7 +175,6 @@ module SecureHeaders end SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) hash = SecureHeaders.header_hash_for(request) - puts hash expect(hash[CSP::REPORT_ONLY]).to be_nil expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:") end @@ -217,6 +216,15 @@ module SecureHeaders end context "setting two headers" do + before(:each) do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + config.csp_report_only = config.csp + end + end + it "sets identical values when the configs are the same" do Configuration.default do |config| config.csp = { @@ -246,13 +254,6 @@ module SecureHeaders end it "allows appending to the enforced policy" do - Configuration.default do |config| - config.csp = { - default_src: %w('self') - } - config.csp_report_only = config.csp - end - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") @@ -260,13 +261,6 @@ module SecureHeaders end it "allows appending to the report only policy" do - Configuration.default do |config| - config.csp = { - default_src: %w('self') - } - config.csp_report_only = config.csp - end - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'") @@ -274,21 +268,32 @@ module SecureHeaders end it "allows appending to both policies" do - Configuration.default do |config| - config.csp = { - default_src: %w('self') - } - config.csp_report_only = config.csp - end - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") end - it "allows overriding the enforced policy" - it "allows overriding the report only policy" - it "allows overriding both policies" + + it "allows overriding the enforced policy", :focus => true do + SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + end + + it "allows overriding the report only policy" do + SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com") + end + + it "allows overriding both policies" do + SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com") + end end end end From d19a55134bd9fc488948f5d63b06eb5216b4a2b8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 8 Aug 2016 15:45:10 -1000 Subject: [PATCH 279/636] don't specify the target since that's not the point of the test --- spec/lib/secure_headers_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 8b36ec76..9e2d9fab 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -99,14 +99,12 @@ module SecureHeaders firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox])) # append an unsupported directive - SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)}, :enforced) + SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)}) # append a supported directive - SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')}, :enforced) + SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')}) hash = SecureHeaders.header_hash_for(firefox_request) - pp hash - # child-src is translated to frame-src expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'") end From c7a57f36951810f10612de217a36bd53f00f6b6c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 16 Aug 2016 14:49:46 -1000 Subject: [PATCH 280/636] ensure that config objects are only dup'd when necessary --- lib/secure_headers.rb | 44 +++++++++++++------ lib/secure_headers/configuration.rb | 12 ----- .../headers/content_security_policy.rb | 2 +- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index b705e170..126f74c8 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -74,18 +74,19 @@ class << self def override_content_security_policy_directives(request, additions, target = nil) config, target = config_and_target(request, target) - if config.csp == OPT_OUT - config.csp = ContentSecurityPolicyConfig.new({}) - end - if config.csp_report_only == OPT_OUT - config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({}) - end - if [:both, :enforced].include?(target) + if config.csp == OPT_OUT + config.csp = ContentSecurityPolicyConfig.new({}) + end + config.csp.merge!(additions) end if [:both, :report_only].include?(target) + if config.csp_report_only == OPT_OUT + config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({}) + end + config.csp_report_only.merge!(additions) end @@ -147,16 +148,18 @@ def opt_out_of_all_protection(request) # returned is meant to be merged into the header value from `@app.call(env)` # in Rack middleware. def header_hash_for(request) - config = config_for(request) + config = config_for(request, prevent_dup = true) + headers = config.cached_headers + if config.csp != OPT_OUT && config.csp.modified? - config.rebuild_csp_header_cache!(request.user_agent, CSP::CONFIG_KEY) + headers = update_cached_csp(config, headers, request.user_agent, CSP::CONFIG_KEY) end if config.csp_report_only != OPT_OUT && config.csp_report_only.modified? - config.rebuild_csp_header_cache!(request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) + headers = update_cached_csp(config, headers, request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) end - use_cached_headers(config.cached_headers, request) + use_cached_headers(headers, request) end # Public: specify which named override will be used for this request. @@ -194,11 +197,15 @@ def content_security_policy_style_nonce(request) # Checks to see if there is an override for this request, then # Checks to see if a named override is used for this request, then # Falls back to the global config - def config_for(request) + def config_for(request, prevent_dup = false) config = request.env[SECURE_HEADERS_CONFIG] || Configuration.get(Configuration::DEFAULT_CONFIG) - if config.frozen? + + # Global configs are frozen, per-request configs are not. When we're not + # making modifications to the config, prevent_dup ensures we don't dup + # the object unnecessarily. It's not necessarily frozen to begin with. + if config.frozen? && !prevent_dup config.dup else config @@ -279,6 +286,17 @@ def use_cached_headers(headers, request) end end + def update_cached_csp(config, headers, user_agent, header_key) + headers = Configuration.send(:deep_copy, headers) + headers[header_key] = {} + + csp = header_key == CSP::CONFIG_KEY ? config.csp : config.csp_report_only + user_agent = UserAgent.parse(user_agent) + variation = CSP.ua_to_variation(user_agent) + headers[header_key][variation] = CSP.make_header(csp, user_agent) + headers + end + # Private: chooses the applicable CSP header for the provided user agent. # # headers - a hash of header_config_key => [header_name, header_value] diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index b85d9e69..6a2ac892 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -155,18 +155,6 @@ def update_x_frame_options(value) self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value) end - # Public: generated cached headers for a specific user agent. - def rebuild_csp_header_cache!(user_agent, header_key) - self.cached_headers[header_key] = {} - - csp = header_key == CSP::CONFIG_KEY ? self.csp : self.csp_report_only - unless csp == OPT_OUT - user_agent = UserAgent.parse(user_agent) - variation = CSP.ua_to_variation(user_agent) - self.cached_headers[header_key][variation] = CSP.make_header(csp, user_agent) - end - end - # Public: validates all configurations values. # # Raises various configuration errors if any invalid config is detected. diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index f55fd620..c1049c16 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -19,7 +19,7 @@ def initialize(config = nil, user_agent = OTHER) elsif config.nil? ContentSecurityPolicyConfig.new(DEFAULT_CONFIG) else - config + config.dup end @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base) From 92dd4e58859767f7ec297cbcb9677ff034f8a851 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 16 Aug 2016 15:07:58 -1000 Subject: [PATCH 281/636] add tests for inferring which policy to modify --- lib/secure_headers.rb | 4 ++- spec/lib/secure_headers_spec.rb | 48 ++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 126f74c8..abb3d0f4 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -228,7 +228,9 @@ def config_and_target(request, target) end def guess_target(config) - if config.csp != OPT_OUT + if config.csp != OPT_OUT && config.csp_report_only != OPT_OUT + :both + elsif config.csp != OPT_OUT :enforced elsif config.csp_report_only != OPT_OUT :report_only diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 9e2d9fab..5c86c28e 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -272,7 +272,7 @@ module SecureHeaders expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") end - it "allows overriding the enforced policy", :focus => true do + it "allows overriding the enforced policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com") @@ -292,6 +292,52 @@ module SecureHeaders expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com") end + + context "when inferring which config to modify" do + it "updates the enforced header when configured" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + end + SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash['Content-Security-Policy-Report-Only']).to be_nil + end + + it "updates the report only header when configured" do + Configuration.default do |config| + config.csp = OPT_OUT + config.csp_report_only = { + default_src: %w('self') + } + end + SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash['Content-Security-Policy']).to be_nil + end + + it "updates both headers if both are configured" do + Configuration.default do |config| + config.csp = { + default_src: %w(enforced.com) + } + config.csp_report_only = { + default_src: %w(reportonly.com) + } + end + SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src enforced.com; script-src enforced.com anothercdn.com") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src reportonly.com; script-src reportonly.com anothercdn.com") + end + + end end end end From 35c702652727f041474b7f23628ded57f1ad01fc Mon Sep 17 00:00:00 2001 From: Olivier Lacan Date: Wed, 17 Aug 2016 10:53:54 -0400 Subject: [PATCH 282/636] Use SVG badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5458c9d9..f0c5dfd3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.png?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.png)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.png)](https://coveralls.io/r/twitter/secureheaders) +# Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) **The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. From 60103a11c3491657bbd2b19797fc7be6b34d8984 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 11:15:28 -1000 Subject: [PATCH 283/636] replace some == OPT_OUT with #opt_out? --- lib/secure_headers.rb | 8 ++++++-- lib/secure_headers/configuration.rb | 2 +- .../headers/content_security_policy_config.rb | 4 ++++ lib/secure_headers/headers/policy_management.rb | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index abb3d0f4..8a3833e0 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -34,6 +34,10 @@ def dup self.class.instance end + def opt_out? + true + end + alias_method :[], :boom alias_method :[]=, :boom alias_method :keys, :boom @@ -75,7 +79,7 @@ def override_content_security_policy_directives(request, additions, target = nil config, target = config_and_target(request, target) if [:both, :enforced].include?(target) - if config.csp == OPT_OUT + if config.csp.opt_out? config.csp = ContentSecurityPolicyConfig.new({}) end @@ -83,7 +87,7 @@ def override_content_security_policy_directives(request, additions, target = nil end if [:both, :report_only].include?(target) - if config.csp_report_only == OPT_OUT + if config.csp_report_only.opt_out? config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({}) end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 6a2ac892..438bfba2 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -272,7 +272,7 @@ def generate_csp_headers(headers) end def generate_csp_headers_for_config(headers, header_key, csp_config) - unless csp_config == OPT_OUT + unless csp_config.opt_out? headers[header_key] = {} CSP::VARIATIONS.each do |name, _| csp = CSP.make_header(csp_config, UserAgent.parse(name)) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index abf22508..20821670 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -60,6 +60,10 @@ def dup self.class.new(self.to_h) end + def opt_out? + self == OPT_OUT + end + def ==(o) self.class == o.class && self.to_h == o.to_h end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index e9921929..186b6eb9 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -200,7 +200,7 @@ def make_header(config, user_agent) # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_config!(config) - return if config.nil? || config == OPT_OUT + return if config.nil? || config.opt_out? raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src) ContentSecurityPolicyConfig.attrs.each do |key| value = config.directive_value(key) From a77ce142ae421b39241ce4d568f4f877d808b1ba Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 11:16:45 -1000 Subject: [PATCH 284/636] remove #idempotent_additions? because it's irrelevent now --- lib/secure_headers/headers/policy_management.rb | 12 ------------ .../headers/policy_management_spec.rb | 14 -------------- 2 files changed, 26 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 186b6eb9..51d76cbc 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -213,18 +213,6 @@ def validate_config!(config) end end - # Public: determine if merging +additions+ will cause a change to the - # actual value of the config. - # - # e.g. config = { script_src: %w(example.org google.com)} and - # additions = { script_src: %w(google.com)} then idempotent_additions? would return - # because google.com is already in the config. - def idempotent_additions?(config, additions) - return true if config == OPT_OUT && additions == OPT_OUT - return false if config == OPT_OUT - config == combine_policies(config, additions) - end - # Public: combine the values from two different configs. # # original - the main config diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index e92dbdd5..73ac7332 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -177,19 +177,5 @@ module SecureHeaders end.to raise_error(ContentSecurityPolicyConfigError) end end - - describe "#idempotent_additions?" do - specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false } - - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true } - specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true } - end end end From df9631015f4ddd225795825b1a31137c42ebbd99 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 11:22:39 -1000 Subject: [PATCH 285/636] very minor cleanup --- lib/secure_headers/configuration.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 438bfba2..096d77c8 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -119,7 +119,7 @@ def deep_copy_if_hash(value) def initialize(&block) self.hpkp = OPT_OUT self.referrer_policy = OPT_OUT - self.csp = ContentSecurityPolicyConfig::DEFAULT + self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT) self.csp_report_only = OPT_OUT instance_eval &block if block_given? end @@ -202,7 +202,7 @@ def csp_report_only=(new_csp) else new_csp = if new_csp.is_a?(ContentSecurityPolicyConfig) ContentSecurityPolicyReportOnlyConfig.new(new_csp.to_h) - elsif new_csp.is_a?(Hash) + else if new_csp[:report_only] == false Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was supplied a config with report_only: false. Use #csp=" end From 2a9021c36e5432f1a23e0018cfa7237e848978ea Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 11:30:26 -1000 Subject: [PATCH 286/636] update docs to show csp/cspro config --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5458c9d9..f4a9b1f2 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ SecureHeaders::Configuration.default do |config| secure: true, # mark all cookies as "Secure" httponly: true, # mark all cookies as "HttpOnly" samesite: { - strict: true # mark all cookies as SameSite=Strict + lax: true # mark all cookies as SameSite=lax } } config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" @@ -48,7 +48,7 @@ SecureHeaders::Configuration.default do |config| config.referrer_policy = "origin-when-cross-origin" config.csp = { # "meta" values. these will shaped the header, but the values are not included in the header. - report_only: true, # default: false + report_only: true, # default: false [DEPRECATED: instead, configure csp_report_only] preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives @@ -69,6 +69,10 @@ SecureHeaders::Configuration.default do |config| upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) } + config.csp_report_only = config.csp.merge({ + img_src: %w(somewhereelse.com), + report_uri: %w(https://report-uri.io/example-csp-report-only) + }) config.hpkp = { report_only: false, max_age: 60.days.to_i, From 3afd1ddabbe7442b4083d87658142feb7e74ec0b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 12:48:01 -1000 Subject: [PATCH 287/636] use #opt_out? helper in another location --- lib/secure_headers.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 8a3833e0..d0d4198d 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -106,11 +106,11 @@ def override_content_security_policy_directives(request, additions, target = nil def append_content_security_policy_directives(request, additions, target = nil) config, target = config_and_target(request, target) - if [:both, :enforced].include?(target) && config.csp != OPT_OUT + if [:both, :enforced].include?(target) && !config.csp.opt_out? config.csp.append(additions) end - if [:both, :report_only].include?(target) && config.csp_report_only != OPT_OUT + if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out? config.csp_report_only.append(additions) end From 8c1c019fe6261eba6090f2329cec6d02668390a9 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 12:51:45 -1000 Subject: [PATCH 288/636] use #opt_out? helper in another location again --- lib/secure_headers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index d0d4198d..c796d31f 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -232,7 +232,7 @@ def config_and_target(request, target) end def guess_target(config) - if config.csp != OPT_OUT && config.csp_report_only != OPT_OUT + if !config.csp.opt_out? && !config.csp_report_only.opt_out? :both elsif config.csp != OPT_OUT :enforced From 7536caeb4d9083870fe1f632f703416200a86d1d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 13:11:34 -1000 Subject: [PATCH 289/636] get rid of even more != OPT_OUT --- lib/secure_headers.rb | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index c796d31f..5b27cd48 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -155,11 +155,11 @@ def header_hash_for(request) config = config_for(request, prevent_dup = true) headers = config.cached_headers - if config.csp != OPT_OUT && config.csp.modified? + if !config.csp.opt_out? && config.csp.modified? headers = update_cached_csp(config, headers, request.user_agent, CSP::CONFIG_KEY) end - if config.csp_report_only != OPT_OUT && config.csp_report_only.modified? + if !config.csp_report_only.opt_out? && config.csp_report_only.modified? headers = update_cached_csp(config, headers, request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) end @@ -234,9 +234,9 @@ def config_and_target(request, target) def guess_target(config) if !config.csp.opt_out? && !config.csp_report_only.opt_out? :both - elsif config.csp != OPT_OUT + elsif !config.csp.opt_out? :enforced - elsif config.csp_report_only != OPT_OUT + elsif !config.csp_report_only.opt_out? :report_only else :both @@ -311,24 +311,6 @@ def update_cached_csp(config, headers, user_agent, header_key) def csp_header_for_ua(headers, request) headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))] end - - # Private: optionally build a header with a given configure - # - # klass - corresponding Class for a given header - # config - A string, symbol, or hash config for the header - # user_agent - A string representing the UA (only used for CSP feature sniffing) - # - # Returns a 2 element array [header_name, header_value] or nil if config - # is OPT_OUT - def make_header(klass, header_config, user_agent = nil) - unless header_config == OPT_OUT - if klass == CSP - klass.make_header(header_config, user_agent) - else - klass.make_header(header_config) - end - end - end end # These methods are mixed into controllers and delegate to the class method From af0c26c9a0dc771e949bf10aef8b575f352485d5 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 13:20:58 -1000 Subject: [PATCH 290/636] remove last bits of == OPT_OUT --- lib/secure_headers/configuration.rb | 26 +++++++------------ .../headers/content_security_policy_config.rb | 2 +- spec/lib/secure_headers_spec.rb | 15 ++++++++++- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 096d77c8..6e122d94 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -180,36 +180,30 @@ def secure_cookies=(secure_cookies) end def csp=(new_csp) - if new_csp == OPT_OUT - @csp = OPT_OUT + if new_csp.respond_to?(:opt_out?) + @csp = new_csp.dup else - if new_csp.is_a?(ContentSecurityPolicyConfig) - @csp = new_csp.dup + if new_csp[:report_only] + Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" + self.csp_report_only = new_csp else - if new_csp[:report_only] - Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" - self.csp_report_only = new_csp - else - @csp = ContentSecurityPolicyConfig.new(new_csp) - end + @csp = ContentSecurityPolicyConfig.new(new_csp) end end end def csp_report_only=(new_csp) - if new_csp == OPT_OUT - @csp_report_only = OPT_OUT - else - new_csp = if new_csp.is_a?(ContentSecurityPolicyConfig) + @csp_report_only = begin + if new_csp.is_a?(ContentSecurityPolicyConfig) ContentSecurityPolicyReportOnlyConfig.new(new_csp.to_h) + elsif new_csp.respond_to?(:opt_out?) + new_csp.dup else if new_csp[:report_only] == false Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was supplied a config with report_only: false. Use #csp=" end ContentSecurityPolicyReportOnlyConfig.new(new_csp) end - - @csp_report_only = new_csp end end diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 20821670..2fad7dc7 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -61,7 +61,7 @@ def dup end def opt_out? - self == OPT_OUT + false end def ==(o) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 5c86c28e..b7b964ab 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -59,7 +59,20 @@ module SecureHeaders end it "allows you to opt out entirely" do - Configuration.default + # configure the disabled-by-default headers to ensure they also do not get set + Configuration.default do |config| + config.csp_report_only = { :default_src => ["example.com"] } + config.hpkp = { + report_only: false, + max_age: 10000000, + include_subdomains: true, + report_uri: "https://report-uri.io/example-hpkp", + pins: [ + {sha256: "abc"}, + {sha256: "123"} + ] + } + end SecureHeaders.opt_out_of_all_protection(request) hash = SecureHeaders.header_hash_for(request) ALL_HEADER_CLASSES.each do |klass| From 1e91f7e0a5bce97136ee08d715c2e1b46ac391e9 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 13:38:38 -1000 Subject: [PATCH 291/636] further separate CSP from CSPRO --- lib/secure_headers.rb | 26 +++++++------ lib/secure_headers/configuration.rb | 6 +-- .../headers/content_security_policy.rb | 6 +-- .../headers/content_security_policy_config.rb | 10 ++++- .../headers/policy_management.rb | 6 +-- .../headers/content_security_policy_spec.rb | 4 +- .../headers/policy_management_spec.rb | 38 +++++++++---------- spec/lib/secure_headers_spec.rb | 6 +-- spec/spec_helper.rb | 2 +- 9 files changed, 52 insertions(+), 52 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 5b27cd48..fb728292 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -47,10 +47,12 @@ def opt_out? SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze HTTPS = "https".freeze - CSP = ContentSecurityPolicy + CSP = ContentSecurityPolicyConfig + CSPRO = ContentSecurityPolicyReportOnlyConfig ALL_HEADER_CLASSES = [ - ContentSecurityPolicy, + ContentSecurityPolicyConfig, + ContentSecurityPolicyReportOnlyConfig, StrictTransportSecurity, PublicKeyPins, ReferrerPolicy, @@ -61,7 +63,7 @@ def opt_out? XXssProtection ].freeze - ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP]).freeze + ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP, CSPRO]).freeze # Headers set on http requests (excludes STS and HPKP) HTTP_HEADER_CLASSES = @@ -160,7 +162,7 @@ def header_hash_for(request) end if !config.csp_report_only.opt_out? && config.csp_report_only.modified? - headers = update_cached_csp(config, headers, request.user_agent, CSP::REPORT_ONLY_CONFIG_KEY) + headers = update_cached_csp(config, headers, request.user_agent, CSPRO::CONFIG_KEY) end use_cached_headers(headers, request) @@ -184,7 +186,7 @@ def use_secure_headers_override(request, name) # # Returns the nonce def content_security_policy_script_nonce(request) - content_security_policy_nonce(request, CSP::SCRIPT_SRC) + content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC) end # Public: gets or creates a nonce for CSP. @@ -193,7 +195,7 @@ def content_security_policy_script_nonce(request) # # Returns the nonce def content_security_policy_style_nonce(request) - content_security_policy_nonce(request, CSP::STYLE_SRC) + content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC) end # Public: Retreives the config for a given header type: @@ -248,7 +250,7 @@ def guess_target(config) # Returns the nonce def content_security_policy_nonce(request, script_or_style) request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp - nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce + nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY]) request.env[NONCE_KEY] end @@ -272,7 +274,7 @@ def header_classes_for(request) HTTP_HEADER_CLASSES end.map do |klass| klass::CONFIG_KEY - end.concat([CSP::REPORT_ONLY_CONFIG_KEY]) + end end # Private: takes a precomputed hash of headers and returns the Headers @@ -282,7 +284,7 @@ def header_classes_for(request) def use_cached_headers(headers, request) header_classes_for(request).each_with_object({}) do |config_key, hash| if header = headers[config_key] - header_name, value = if [CSP::CONFIG_KEY, CSP::REPORT_ONLY_CONFIG_KEY].include?(config_key) + header_name, value = if [CSP::CONFIG_KEY, CSPRO::CONFIG_KEY].include?(config_key) csp_header_for_ua(header, request) else header @@ -298,8 +300,8 @@ def update_cached_csp(config, headers, user_agent, header_key) csp = header_key == CSP::CONFIG_KEY ? config.csp : config.csp_report_only user_agent = UserAgent.parse(user_agent) - variation = CSP.ua_to_variation(user_agent) - headers[header_key][variation] = CSP.make_header(csp, user_agent) + variation = ContentSecurityPolicy.ua_to_variation(user_agent) + headers[header_key][variation] = ContentSecurityPolicy.make_header(csp, user_agent) headers end @@ -309,7 +311,7 @@ def update_cached_csp(config, headers, user_agent, header_key) # # Returns a CSP [header, value] array def csp_header_for_ua(headers, request) - headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))] + headers[ContentSecurityPolicy.ua_to_variation(UserAgent.parse(request.user_agent))] end end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 6e122d94..6d5e1a79 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -262,14 +262,14 @@ def cache_headers! # Returns nothing def generate_csp_headers(headers) generate_csp_headers_for_config(headers, CSP::CONFIG_KEY, self.csp) - generate_csp_headers_for_config(headers, CSP::REPORT_ONLY_CONFIG_KEY, self.csp_report_only) + generate_csp_headers_for_config(headers, CSPRO::CONFIG_KEY, self.csp_report_only) end def generate_csp_headers_for_config(headers, header_key, csp_config) unless csp_config.opt_out? headers[header_key] = {} - CSP::VARIATIONS.each do |name, _| - csp = CSP.make_header(csp_config, UserAgent.parse(name)) + ContentSecurityPolicy::VARIATIONS.each do |name, _| + csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name)) headers[header_key][name] = csp.freeze end end diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index c1049c16..c67350f1 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -37,11 +37,7 @@ def initialize(config = nil, user_agent = OTHER) # Returns the name to use for the header. Either "Content-Security-Policy" or # "Content-Security-Policy-Report-Only" def name - if @config.report_only? - REPORT_ONLY - else - HEADER_NAME - end + @config.class.const_get(:HEADER_NAME) end ## diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 2fad7dc7..6bf28aae 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -39,7 +39,7 @@ def modified? end def merge(new_hash) - CSP.combine_policies(self.to_h, new_hash) + ContentSecurityPolicy.combine_policies(self.to_h, new_hash) end def merge!(new_hash) @@ -47,7 +47,7 @@ def merge!(new_hash) end def append(new_hash) - from_hash(CSP.combine_policies(self.to_h, new_hash)) + from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash)) end def to_h @@ -85,6 +85,9 @@ def from_hash(hash) class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicyConfig + CONFIG_KEY = :csp + HEADER_NAME = "Content-Security-Policy".freeze + def self.attrs PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES end @@ -107,6 +110,9 @@ def report_only? end class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig + CONFIG_KEY = :csp_report_only + HEADER_NAME = "Content-Security-Policy-Report-Only".freeze + def report_only? true end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 51d76cbc..87bed936 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -7,9 +7,6 @@ def self.included(base) MODERN_BROWSERS = %w(Chrome Opera Firefox) DEFAULT_VALUE = "default-src https:".freeze DEFAULT_CONFIG = { default_src: %w(https:) }.freeze - HEADER_NAME = "Content-Security-Policy".freeze - REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze - HEADER_NAMES = [HEADER_NAME, REPORT_ONLY] DATA_PROTOCOL = "data:".freeze BLOB_PROTOCOL = "blob:".freeze SELF = "'self'".freeze @@ -162,8 +159,7 @@ def self.included(base) UPGRADE_INSECURE_REQUESTS => :boolean }.freeze - CONFIG_KEY = :csp - REPORT_ONLY_CONFIG_KEY = :csp_report_only + STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) HTTP_SCHEME_REGEX = %r{\Ahttps?://} diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 1e30f62b..ef0fd8ce 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -14,11 +14,11 @@ module SecureHeaders describe "#name" do context "when in report-only mode" do - specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") } + specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) } end context "when in enforce mode" do - specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicy::HEADER_NAME) } + specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) } end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 73ac7332..a41d7926 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -40,61 +40,61 @@ module SecureHeaders report_uri: %w(https://example.com/uri-directive) } - CSP.validate_config!(ContentSecurityPolicyConfig.new(config)) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config)) end it "requires a :default_src value" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self'))) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :report_only to be a truthy value" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve"))) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :preserve_schemes to be a truthy value" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve"))) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :block_all_mixed_content to be a boolean value" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve"))) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :upgrade_insecure_requests to be a boolean value" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve"))) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires all source lists to be an array of strings" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve")) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve")) end.to raise_error(ContentSecurityPolicyConfigError) end it "allows nil values" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil])) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil])) end.to_not raise_error end it "rejects unknown directives / config" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve")) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve")) end.to raise_error(ContentSecurityPolicyConfigError) end # this is mostly to ensure people don't use the antiquated shorthands common in other configs it "performs light validation on source lists" do expect do - CSP.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval))) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval))) end.to raise_error(ContentSecurityPolicyConfigError) end end @@ -106,7 +106,7 @@ module SecureHeaders default_src: %w(https:) } end - combined_config = CSP.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(CSP::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") @@ -120,7 +120,7 @@ module SecureHeaders }.freeze end report_uri = "https://report-uri.io/asdf" - combined_config = CSP.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri]) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri]) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.value).to include("report-uri #{report_uri}") end @@ -132,12 +132,12 @@ module SecureHeaders report_only: false }.freeze end - non_default_source_additions = CSP::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| + non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end - combined_config = CSP.combine_policies(Configuration.get.csp.to_h, non_default_source_additions) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, non_default_source_additions) - CSP::NON_FETCH_SOURCES.each do |directive| + ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) end @@ -151,9 +151,9 @@ module SecureHeaders report_only: false } end - combined_config = CSP.combine_policies(Configuration.get.csp.to_h, report_only: true) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_only: true) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) - expect(csp.name).to eq(CSP::REPORT_ONLY) + expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) end it "overrides the :block_all_mixed_content flag" do @@ -163,7 +163,7 @@ module SecureHeaders block_all_mixed_content: false } end - combined_config = CSP.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to eq("default-src https:; block-all-mixed-content") end @@ -173,7 +173,7 @@ module SecureHeaders config.csp = OPT_OUT end expect do - CSP.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) + ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) end.to raise_error(ContentSecurityPolicyConfigError) end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index b7b964ab..20a79f28 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -33,7 +33,7 @@ module SecureHeaders config.csp_report_only = { default_src: %w('self')} # no default value end SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY) - SecureHeaders.opt_out_of_header(request, CSP::REPORT_ONLY_CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, CSPRO::CONFIG_KEY) SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy-Report-Only']).to be_nil @@ -186,7 +186,7 @@ module SecureHeaders end SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) hash = SecureHeaders.header_hash_for(request) - expect(hash[CSP::REPORT_ONLY]).to be_nil + expect(hash[CSPRO::HEADER_NAME]).to be_nil expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:") end @@ -367,7 +367,7 @@ module SecureHeaders it "validates your csp config upon configuration" do expect do Configuration.default do |config| - config.csp = { CSP::DEFAULT_SRC => '123456' } + config.csp = { ContentSecurityPolicy::DEFAULT_SRC => '123456' } end end.to raise_error(ContentSecurityPolicyConfigError) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 23fc8f62..20e8eb82 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,7 +25,7 @@ } def expect_default_values(hash) - expect(hash[SecureHeaders::CSP::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'") + expect(hash[SecureHeaders::ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'") expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) From 9112c625e7220fa78df567ff54f45ef6cd5ca5b8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 13:40:16 -1000 Subject: [PATCH 292/636] remove CSP/CSPRO shortcut constants --- lib/secure_headers.rb | 15 +++++++------- lib/secure_headers/configuration.rb | 4 ++-- .../headers/policy_management_spec.rb | 2 +- spec/lib/secure_headers/middleware_spec.rb | 2 +- spec/lib/secure_headers/view_helpers_spec.rb | 8 ++++---- spec/lib/secure_headers_spec.rb | 20 +++++++++---------- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index fb728292..96fe790f 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -47,8 +47,6 @@ def opt_out? SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze HTTPS = "https".freeze - CSP = ContentSecurityPolicyConfig - CSPRO = ContentSecurityPolicyReportOnlyConfig ALL_HEADER_CLASSES = [ ContentSecurityPolicyConfig, @@ -63,7 +61,10 @@ def opt_out? XXssProtection ].freeze - ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP, CSPRO]).freeze + ALL_HEADERS_BESIDES_CSP = ( + ALL_HEADER_CLASSES - + [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig] + ).freeze # Headers set on http requests (excludes STS and HPKP) HTTP_HEADER_CLASSES = @@ -158,11 +159,11 @@ def header_hash_for(request) headers = config.cached_headers if !config.csp.opt_out? && config.csp.modified? - headers = update_cached_csp(config, headers, request.user_agent, CSP::CONFIG_KEY) + headers = update_cached_csp(config, headers, request.user_agent, ContentSecurityPolicyConfig::CONFIG_KEY) end if !config.csp_report_only.opt_out? && config.csp_report_only.modified? - headers = update_cached_csp(config, headers, request.user_agent, CSPRO::CONFIG_KEY) + headers = update_cached_csp(config, headers, request.user_agent, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY) end use_cached_headers(headers, request) @@ -284,7 +285,7 @@ def header_classes_for(request) def use_cached_headers(headers, request) header_classes_for(request).each_with_object({}) do |config_key, hash| if header = headers[config_key] - header_name, value = if [CSP::CONFIG_KEY, CSPRO::CONFIG_KEY].include?(config_key) + header_name, value = if [ContentSecurityPolicyConfig::CONFIG_KEY, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY].include?(config_key) csp_header_for_ua(header, request) else header @@ -298,7 +299,7 @@ def update_cached_csp(config, headers, user_agent, header_key) headers = Configuration.send(:deep_copy, headers) headers[header_key] = {} - csp = header_key == CSP::CONFIG_KEY ? config.csp : config.csp_report_only + csp = header_key == ContentSecurityPolicyConfig::CONFIG_KEY ? config.csp : config.csp_report_only user_agent = UserAgent.parse(user_agent) variation = ContentSecurityPolicy.ua_to_variation(user_agent) headers[header_key][variation] = ContentSecurityPolicy.make_header(csp, user_agent) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 6d5e1a79..97ddc790 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -261,8 +261,8 @@ def cache_headers! # # Returns nothing def generate_csp_headers(headers) - generate_csp_headers_for_config(headers, CSP::CONFIG_KEY, self.csp) - generate_csp_headers_for_config(headers, CSPRO::CONFIG_KEY, self.csp_report_only) + generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp) + generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only) end def generate_csp_headers_for_config(headers, header_key, csp_config) diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index a41d7926..f711d5f0 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -108,7 +108,7 @@ module SecureHeaders end combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) - expect(csp.name).to eq(CSP::HEADER_NAME) + expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index a84e288e..13fbf8c9 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -51,7 +51,7 @@ module SecureHeaders SecureHeaders.use_secure_headers_override(request, "my_custom_config") expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config")) _, env = middleware.call request.env - expect(env[CSP::HEADER_NAME]).to match("example.org") + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org") end context "secure_cookies" do diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 386129de..17723853 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -118,10 +118,10 @@ module SecureHeaders Message.new(request).result _, env = middleware.call request.env - expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/) - expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/) - expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/) - expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/) + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/) + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/) + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) end end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 20a79f28..6d3d9a73 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -16,7 +16,7 @@ module SecureHeaders it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do expect do - SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY) end.to raise_error(Configuration::NotYetConfiguredError) end @@ -32,8 +32,8 @@ module SecureHeaders Configuration.default do |config| config.csp_report_only = { default_src: %w('self')} # no default value end - SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY) - SecureHeaders.opt_out_of_header(request, CSPRO::CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY) SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy-Report-Only']).to be_nil @@ -98,7 +98,7 @@ module SecureHeaders SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self')) hash = SecureHeaders.header_hash_for(request) - expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'") expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN) end @@ -119,7 +119,7 @@ module SecureHeaders hash = SecureHeaders.header_hash_for(firefox_request) # child-src is translated to frame-src - expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'") end it "produces a hash of headers with default config" do @@ -164,7 +164,7 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) hash = SecureHeaders.header_hash_for(request) - expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end it "overrides individual directives" do @@ -175,7 +175,7 @@ module SecureHeaders end SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none')) hash = SecureHeaders.header_hash_for(request) - expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'") end it "overrides non-existant directives" do @@ -186,8 +186,8 @@ module SecureHeaders end SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) hash = SecureHeaders.header_hash_for(request) - expect(hash[CSPRO::HEADER_NAME]).to be_nil - expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:") + expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:") end it "does not append a nonce when the browser does not support it" do @@ -202,7 +202,7 @@ module SecureHeaders safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5])) nonce = SecureHeaders.content_security_policy_script_nonce(safari_request) hash = SecureHeaders.header_hash_for(safari_request) - expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'") end it "appends a nonce to the script-src when used" do From 6d9d76a7b22ee28374b0da21a56d02b53269a14b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 13:56:08 -1000 Subject: [PATCH 293/636] remove unnecessary special handling of CSP/CSPRO now that they are separate classes --- lib/secure_headers.rb | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 96fe790f..91b298da 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -159,11 +159,11 @@ def header_hash_for(request) headers = config.cached_headers if !config.csp.opt_out? && config.csp.modified? - headers = update_cached_csp(config, headers, request.user_agent, ContentSecurityPolicyConfig::CONFIG_KEY) + headers = update_cached_csp(config.csp, headers, request.user_agent) end if !config.csp_report_only.opt_out? && config.csp_report_only.modified? - headers = update_cached_csp(config, headers, request.user_agent, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY) + headers = update_cached_csp(config.csp_report_only, headers, request.user_agent) end use_cached_headers(headers, request) @@ -273,8 +273,6 @@ def header_classes_for(request) ALL_HEADER_CLASSES else HTTP_HEADER_CLASSES - end.map do |klass| - klass::CONFIG_KEY end end @@ -283,9 +281,9 @@ def header_classes_for(request) # # Returns a hash of header names / values valid for a given request. def use_cached_headers(headers, request) - header_classes_for(request).each_with_object({}) do |config_key, hash| - if header = headers[config_key] - header_name, value = if [ContentSecurityPolicyConfig::CONFIG_KEY, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY].include?(config_key) + header_classes_for(request).each_with_object({}) do |klass, hash| + if header = headers[klass::CONFIG_KEY] + header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass) csp_header_for_ua(header, request) else header @@ -295,14 +293,13 @@ def use_cached_headers(headers, request) end end - def update_cached_csp(config, headers, user_agent, header_key) + def update_cached_csp(config, headers, user_agent) headers = Configuration.send(:deep_copy, headers) - headers[header_key] = {} + headers[config.class::CONFIG_KEY] = {} - csp = header_key == ContentSecurityPolicyConfig::CONFIG_KEY ? config.csp : config.csp_report_only user_agent = UserAgent.parse(user_agent) variation = ContentSecurityPolicy.ua_to_variation(user_agent) - headers[header_key][variation] = ContentSecurityPolicy.make_header(csp, user_agent) + headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent) headers end From 1b80f85be36152c3d25f7ff699f51422c3c3ffa4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 14:02:39 -1000 Subject: [PATCH 294/636] inline method so we're not creating UserAgent objects everywhere --- lib/secure_headers.rb | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 91b298da..3c98007a 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -157,16 +157,26 @@ def opt_out_of_all_protection(request) def header_hash_for(request) config = config_for(request, prevent_dup = true) headers = config.cached_headers + user_agent = UserAgent.parse(request.user_agent) if !config.csp.opt_out? && config.csp.modified? - headers = update_cached_csp(config.csp, headers, request.user_agent) + headers = update_cached_csp(config.csp, headers, user_agent) end if !config.csp_report_only.opt_out? && config.csp_report_only.modified? - headers = update_cached_csp(config.csp_report_only, headers, request.user_agent) + headers = update_cached_csp(config.csp_report_only, headers, user_agent) end - use_cached_headers(headers, request) + header_classes_for(request).each_with_object({}) do |klass, hash| + if header = headers[klass::CONFIG_KEY] + header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass) + csp_header_for_ua(header, user_agent) + else + header + end + hash[header_name] = value + end + end end # Public: specify which named override will be used for this request. @@ -276,28 +286,9 @@ def header_classes_for(request) end end - # Private: takes a precomputed hash of headers and returns the Headers - # customized for the request. - # - # Returns a hash of header names / values valid for a given request. - def use_cached_headers(headers, request) - header_classes_for(request).each_with_object({}) do |klass, hash| - if header = headers[klass::CONFIG_KEY] - header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass) - csp_header_for_ua(header, request) - else - header - end - hash[header_name] = value - end - end - end - def update_cached_csp(config, headers, user_agent) headers = Configuration.send(:deep_copy, headers) headers[config.class::CONFIG_KEY] = {} - - user_agent = UserAgent.parse(user_agent) variation = ContentSecurityPolicy.ua_to_variation(user_agent) headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent) headers @@ -308,8 +299,8 @@ def update_cached_csp(config, headers, user_agent) # headers - a hash of header_config_key => [header_name, header_value] # # Returns a CSP [header, value] array - def csp_header_for_ua(headers, request) - headers[ContentSecurityPolicy.ua_to_variation(UserAgent.parse(request.user_agent))] + def csp_header_for_ua(headers, user_agent) + headers[ContentSecurityPolicy.ua_to_variation(user_agent)] end end From 8e14907adb94048a8b8ca0d3cbc6b51b60642d5b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 14:07:57 -1000 Subject: [PATCH 295/636] don't modify the original config object, removing the need to dup it --- .../headers/content_security_policy.rb | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index c67350f1..a7242cc5 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -19,7 +19,7 @@ def initialize(config = nil, user_agent = OTHER) elsif config.nil? ContentSecurityPolicyConfig.new(DEFAULT_CONFIG) else - config.dup + config end @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base) @@ -115,15 +115,15 @@ def build_directive(directive) # If a directive contains 'none' but has other values, 'none' is ommitted. # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression) def minify_source_list(directive, source_list) - source_list.compact! + source_list = source_list.compact if source_list.include?(STAR) keep_wildcard_sources(source_list) else - populate_nonces!(directive, source_list) - reject_all_values_if_none!(source_list) + source_list = populate_nonces(directive, source_list) + source_list = reject_all_values_if_none(source_list) unless directive == REPORT_URI || @preserve_schemes - strip_source_schemes!(source_list) + source_list = strip_source_schemes(source_list) end dedup_source_list(source_list) end @@ -135,8 +135,12 @@ def keep_wildcard_sources(source_list) end # Discard any 'none' values if more directives are supplied since none may override values. - def reject_all_values_if_none!(source_list) - source_list.reject! { |value| value == NONE } if source_list.length > 1 + def reject_all_values_if_none(source_list) + if source_list.length > 1 + source_list.reject { |value| value == NONE } + else + source_list + end end # Removes duplicates and sources that already match an existing wild card. @@ -158,12 +162,14 @@ def dedup_source_list(sources) # Private: append a nonce to the script/style directories if script_nonce # or style_nonce are provided. - def populate_nonces!(directive, source_list) + def populate_nonces(directive, source_list) case directive when SCRIPT_SRC append_nonce(source_list, @script_nonce) when STYLE_SRC append_nonce(source_list, @style_nonce) + else + source_list end end @@ -180,6 +186,8 @@ def append_nonce(source_list, nonce) source_list << UNSAFE_INLINE end end + + source_list end # Private: return the list of directives that are supported by the user agent, @@ -193,8 +201,8 @@ def directives end # Private: Remove scheme from source expressions. - def strip_source_schemes!(source_list) - source_list.map! { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } + def strip_source_schemes(source_list) + source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } end # Private: determine which directives are supported for the given user agent. From dc1bbac3c878222c92e503890cba2c5a440d67d3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 14:17:06 -1000 Subject: [PATCH 296/636] ensure that the deprecated way of using the report_only header still works :) --- lib/secure_headers/configuration.rb | 2 ++ spec/lib/secure_headers_spec.rb | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 97ddc790..53b3d965 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -184,7 +184,9 @@ def csp=(new_csp) @csp = new_csp.dup else if new_csp[:report_only] + # Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" + @csp = OPT_OUT self.csp_report_only = new_csp else @csp = ContentSecurityPolicyConfig.new(new_csp) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 6d3d9a73..6407e961 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -226,6 +226,24 @@ module SecureHeaders expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") end + it "supports the deprecated `report_only: true` format" do + expect(Kernel).to receive(:warn).once + + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + report_only: true + } + end + + expect(Configuration.get.csp).to eq(OPT_OUT) + expect(Configuration.get.csp_report_only).to be_a(ContentSecurityPolicyReportOnlyConfig) + + hash = SecureHeaders.header_hash_for(request) + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil + expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'") + end + context "setting two headers" do before(:each) do Configuration.default do |config| From 7600b900c6663883a321719882616652f72e6386 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 17 Aug 2016 14:25:03 -1000 Subject: [PATCH 297/636] add guard ensuring people aren't setting csp_report_only with report_only: false --- lib/secure_headers/configuration.rb | 7 ++++--- spec/lib/secure_headers_spec.rb | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 53b3d965..07a7cf86 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -201,10 +201,11 @@ def csp_report_only=(new_csp) elsif new_csp.respond_to?(:opt_out?) new_csp.dup else - if new_csp[:report_only] == false - Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was supplied a config with report_only: false. Use #csp=" + if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise + raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp=" + else + ContentSecurityPolicyReportOnlyConfig.new(new_csp) end - ContentSecurityPolicyReportOnlyConfig.new(new_csp) end end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 6407e961..99b63c61 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -244,6 +244,17 @@ module SecureHeaders expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'") end + it "Raises an error if csp_report_only is used with `report_only: false`" do + expect do + Configuration.default do |config| + config.csp_report_only = { + default_src: %w('self'), + report_only: false + } + end + end.to raise_error(ContentSecurityPolicyConfigError) + end + context "setting two headers" do before(:each) do Configuration.default do |config| From 3225ed600306d8c7e90ecf4d6849a9f17e0d9ac0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 18 Aug 2016 12:31:51 -1000 Subject: [PATCH 298/636] infer default policies when incomplete CSP configuration is supplied. --- README.md | 27 ++++++++++- lib/secure_headers/configuration.rb | 15 +++++- .../headers/content_security_policy_config.rb | 8 ++++ spec/lib/secure_headers_spec.rb | 46 ++++++++++++++++++- 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f4a9b1f2..71da2ce7 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,32 @@ use SecureHeaders::Middleware ## Default values -All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). +All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is: + +``` +Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' +Strict-Transport-Security: max-age=631138519 +X-Content-Type-Options: nosniff +X-Download-Options: noopen +X-Frame-Options: sameorigin +X-Permitted-Cross-Domain-Policies: none +X-Xss-Protection: 1; mode=block +``` + +### Default CSP + +By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. + +```ruby +Configuration.default do |config| + config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. + config.csp_report_only = { + default_src: %w('self') + } +end +``` + +If ** ## Named overrides diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 07a7cf86..0a5d5657 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -194,10 +194,16 @@ def csp=(new_csp) end end + # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot + # contain `report_only: false` or an error will be raised. + # + # NOTE: if csp has not been configured/has the default value when + # configuring csp_report_only, the code will assume you mean to only use + # report-only mode and you will be opted-out of enforce mode. def csp_report_only=(new_csp) @csp_report_only = begin - if new_csp.is_a?(ContentSecurityPolicyConfig) - ContentSecurityPolicyReportOnlyConfig.new(new_csp.to_h) + if new_csp.is_a?(ContentSecurityPolicyConfig) + new_csp.make_report_only elsif new_csp.respond_to?(:opt_out?) new_csp.dup else @@ -208,6 +214,11 @@ def csp_report_only=(new_csp) end end end + + if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT + Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp=" + @csp = OPT_OUT + end end protected diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 6bf28aae..52d5c673 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -107,6 +107,10 @@ def self.attrs def report_only? false end + + def make_report_only + ContentSecurityPolicyReportOnlyConfig.new(self.to_h) + end end class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig @@ -116,5 +120,9 @@ class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig def report_only? true end + + def make_report_only + self + end end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 99b63c61..c0e49045 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -30,7 +30,8 @@ module SecureHeaders describe "#header_hash_for" do it "allows you to opt out of individual headers via API" do Configuration.default do |config| - config.csp_report_only = { default_src: %w('self')} # no default value + config.csp = { default_src: %w('self')} + config.csp_report_only = config.csp end SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY) SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY) @@ -61,7 +62,8 @@ module SecureHeaders it "allows you to opt out entirely" do # configure the disabled-by-default headers to ensure they also do not get set Configuration.default do |config| - config.csp_report_only = { :default_src => ["example.com"] } + config.csp = { :default_src => ["example.com"] } + config.csp_report_only = config.csp config.hpkp = { report_only: false, max_age: 10000000, @@ -293,6 +295,46 @@ module SecureHeaders expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self'") end + it "allows you to opt-out of enforced CSP" do + Configuration.default do |config| + config.csp = SecureHeaders::OPT_OUT + config.csp_report_only = { + default_src: %w('self') + } + end + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to be_nil + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + end + + it "opts-out of enforced CSP when only csp_report_only is set" do + expect(Kernel).to receive(:warn).once + Configuration.default do |config| + config.csp_report_only = { + default_src: %w('self') + } + end + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to be_nil + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + end + + it "allows you to set csp_report_only before csp" do + expect(Kernel).to receive(:warn).once + Configuration.default do |config| + config.csp_report_only = { + default_src: %w('self') + } + config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')}) + end + + hash = SecureHeaders.header_hash_for(request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'") + expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + end + it "allows appending to the enforced policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) From 2b06ada4fb696f2e68c6b3ea73c37ade3acdb6da Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 18 Aug 2016 13:19:19 -1000 Subject: [PATCH 299/636] add support for named configs --- README.md | 55 +++++++++++++++++++++++++++++ lib/secure_headers.rb | 9 +++++ lib/secure_headers/configuration.rb | 11 ++++++ spec/lib/secure_headers_spec.rb | 22 ++++++++++++ 4 files changed, 97 insertions(+) diff --git a/README.md b/README.md index f0c5dfd3..7e908c22 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,61 @@ use SecureHeaders::Middleware All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). +## Named Appends + +Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. + +```ruby +def show + if include_widget? + @widget = widget.render + use_content_security_policy_named_append(:widget_partial) + end +end + + +SecureHeaders::Configuration.named_append(:widget_partial) do |request| + if request.controller_instance.current_user.in_test_bucket? + { child_src: %w(beta.thirdpartyhost.com) } + else + { child_src: %w(thirdpartyhost.com) } + end +end +``` + +You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following: + +```ruby +SecureHeader::Configuration.default do |config| + config.csp = { default_src: %w('self')} +end + +SecureHeaders::Configuration.named_append(:A) do |request| + { default_src: %w(myhost.com) } +end + +SecureHeaders::Configuration.named_append(:B) do |request| + { script_src: %w('unsafe-eval') } +end +``` + +The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.): + +```ruby +def index + use_content_security_policy_named_append(:A) + use_content_security_policy_named_append(:B) + # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval'; +end + +def show + use_content_security_policy_named_append(:B) + use_content_security_policy_named_append(:A) + # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval'; +end +``` + + ## Named overrides Named overrides serve two purposes: diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index badfdfd0..ddfdb106 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -72,6 +72,11 @@ def append_content_security_policy_directives(request, additions) override_secure_headers_request_config(request, config) end + def use_content_security_policy_named_append(request, name) + additions = SecureHeaders::Configuration.named_appends(name).call(request) + append_content_security_policy_directives(request, additions) + end + # Public: override X-Frame-Options settings for this request. # # value - deny, sameorigin, or allowall @@ -267,4 +272,8 @@ def override_content_security_policy_directives(additions) def override_x_frame_options(value) SecureHeaders.override_x_frame_options(request, value) end + + def use_content_security_policy_named_append(name) + SecureHeaders.use_content_security_policy_named_append(request, name) + end end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 59c42c59..55440794 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -7,6 +7,8 @@ class Configuration class NotYetConfiguredError < StandardError; end class IllegalPolicyModificationError < StandardError; end class << self + @@appends = {} + # Public: Set the global default configuration. # # Optionally supply a block to override the defaults set by this library. @@ -46,6 +48,15 @@ def get(name = DEFAULT_CONFIG) @configurations[name] end + def named_appends(name) + @@appends[name] + end + + def named_append(name, target = nil, &block) + raise "Provide a configuration block" unless block_given? + @@appends[name] = block + end + private # Private: add a valid configuration to the global set of named configs. diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 4a07c107..259206ca 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -151,6 +151,28 @@ module SecureHeaders expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end + it "supports named appends" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + end + + Configuration.named_append(:moar_default_sources) do |request| + { default_src: %w(https:)} + end + + Configuration.named_append(:how_about_a_script_src_too) do |request| + { script_src: %w('unsafe-inline')} + end + + SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources) + SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too) + hash = SecureHeaders.header_hash_for(request) + + expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'") + end + it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do Configuration.default do |config| config.csp = { From 7abe764d80762c6367736efd27855fa403151f84 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 18 Aug 2016 13:28:28 -1000 Subject: [PATCH 300/636] use @ instead of @@ --- lib/secure_headers/configuration.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 55440794..174dc07c 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -7,8 +7,6 @@ class Configuration class NotYetConfiguredError < StandardError; end class IllegalPolicyModificationError < StandardError; end class << self - @@appends = {} - # Public: Set the global default configuration. # # Optionally supply a block to override the defaults set by this library. @@ -49,12 +47,14 @@ def get(name = DEFAULT_CONFIG) end def named_appends(name) - @@appends[name] + @appends ||= {} + @appends[name] end def named_append(name, target = nil, &block) + @appends ||= {} raise "Provide a configuration block" unless block_given? - @@appends[name] = block + @appends[name] = block end private From 16b0b0c92a724406838be65099d294027e67eb85 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 6 Sep 2016 11:00:27 -1000 Subject: [PATCH 301/636] handle the case where nonces weren't populated for default csp configs using the nonce helpers --- .../headers/policy_management.rb | 41 +++++++++++++------ spec/lib/secure_headers_spec.rb | 29 ++++++++++++- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 58e0eb7b..f7b49932 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -53,16 +53,6 @@ def self.included(base) FRAME_ANCESTORS = :frame_ancestors PLUGIN_TYPES = :plugin_types - # These are directives that do not inherit the default-src value. This is - # useful when calling #combine_policies. - NON_FETCH_SOURCES = [ - BASE_URI, - FORM_ACTION, - FRAME_ANCESTORS, - PLUGIN_TYPES, - REPORT_URI - ] - DIRECTIVES_2_0 = [ DIRECTIVES_1_0, BASE_URI, @@ -127,6 +117,18 @@ def self.included(base) # everything else is in between. BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] + # These are directives that do not inherit the default-src value. This is + # useful when calling #combine_policies. + NON_FETCH_SOURCES = [ + BASE_URI, + FORM_ACTION, + FRAME_ANCESTORS, + PLUGIN_TYPES, + REPORT_URI + ] + + FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES + VARIATIONS = { "Chrome" => CHROME_DIRECTIVES, "Opera" => CHROME_DIRECTIVES, @@ -268,8 +270,23 @@ def merge_policy_additions(original, additions) def populate_fetch_source_with_default!(original, additions) # in case we would be appending to an empty directive, fill it with the default-src value additions.keys.each do |directive| - unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive) - original[directive] = original[:default_src] + if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions)) + if nonce_added?(original, additions) + inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym + unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive) + original[inferred_directive] = original[:default_src] + end + else + original[directive] = original[:default_src] + end + end + end + end + + def nonce_added?(original, additions) + [:script_nonce, :style_nonce].each do |nonce| + if additions[nonce] && !original[nonce] + return true end end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 4a07c107..4497d7cb 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -138,6 +138,10 @@ module SecureHeaders end context "content security policy" do + let(:chrome_request) { + Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome])) + } + it "appends a value to csp directive" do Configuration.default do |config| config.csp = { @@ -151,6 +155,30 @@ module SecureHeaders expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end + it "appends a nonce to a missing script-src value" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + end + + SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header + hash = SecureHeaders.header_hash_for(chrome_request) + expect(hash[CSP::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/ + end + + it "appends a hash to a missing script-src value" do + Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + end + + SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123')) + hash = SecureHeaders.header_hash_for(chrome_request) + expect(hash[CSP::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/ + end + it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do Configuration.default do |config| config.csp = { @@ -234,7 +262,6 @@ module SecureHeaders } end - chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome])) nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request) # simulate the nonce being used multiple times in a request: From f083d8ce92c158bcf3c6566a085b568fc5a42a52 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 8 Sep 2016 13:52:03 -1000 Subject: [PATCH 302/636] version bump and docs --- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + secure_headers.gemspec | 2 +- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ddc15f..4b9e5eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ +## 3.4.1 Named Appends + +### Small bugfix + +If your CSP did not define a script/style-src and you tried to use a script/style nonce, the nonce would be added to the page but it would not be added to the CSP. A workaround is to define a script/style src but now it should add the missing directive (and populate it with the default-src). + +### Named Appends + +Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. + +```ruby +def show + if include_widget? + @widget = widget.render + use_content_security_policy_named_append(:widget_partial) + end +end + + +SecureHeaders::Configuration.named_append(:widget_partial) do |request| + if request.controller_instance.current_user.in_test_bucket? + SecureHeaders.override_x_frame_options(request, "DENY") + { child_src: %w(beta.thirdpartyhost.com) } + else + { child_src: %w(thirdpartyhost.com) } + end +end +``` + +You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following: + +```ruby +SecureHeader::Configuration.default do |config| + config.csp = { default_src: %w('self')} +end + +SecureHeaders::Configuration.named_append(:A) do |request| + { default_src: %w(myhost.com) } +end + +SecureHeaders::Configuration.named_append(:B) do |request| + { script_src: %w('unsafe-eval') } +end +``` + +The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.): + +```ruby +def index + use_content_security_policy_named_append(:A) + use_content_security_policy_named_append(:B) + # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval'; +end + +def show + use_content_security_policy_named_append(:B) + use_content_security_policy_named_append(:A) + # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval'; +end +``` + ## 3.4.0 the frame-src/child-src transition for Firefox. Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here: diff --git a/README.md b/README.md index 7e908c22..5a65a50f 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ end SecureHeaders::Configuration.named_append(:widget_partial) do |request| + SecureHeaders.override_x_frame_options(request, "DENY") if request.controller_instance.current_user.in_test_bucket? { child_src: %w(beta.thirdpartyhost.com) } else diff --git a/secure_headers.gemspec b/secure_headers.gemspec index b1055ce1..d43aca1e 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.4.0" + gem.version = "3.4.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 3d25454e6a20fd48a27d401248f1b4d4138c93cc Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 8 Sep 2016 14:14:53 -1000 Subject: [PATCH 303/636] bump for prerelease w/ two csp headers --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9e5eef..457f470b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.5.0.pre + +This release adds support for setting two CSP headers (enforced/report-only) and management around them. + ## 3.4.1 Named Appends ### Small bugfix diff --git a/secure_headers.gemspec b/secure_headers.gemspec index d43aca1e..4f2b9376 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.4.1" + gem.version = "3.5.0.pre" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 615136015538339bd42ee250d78cd0dedb8f638f Mon Sep 17 00:00:00 2001 From: Brian Tsai Date: Sun, 11 Sep 2016 06:16:41 +0900 Subject: [PATCH 304/636] Fixing README documentation on csp_report_only `csp_report_only` is available only in 3.5.0_pre and onwards, but if you run bundle install with `gem 'secure_headers`` in your Gemfile, it will pull 3.4.1 which doesn't have `csp_report_only` available. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b31e573a..3d951f33 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ SecureHeaders::Configuration.default do |config| config.referrer_policy = "origin-when-cross-origin" config.csp = { # "meta" values. these will shaped the header, but the values are not included in the header. - report_only: true, # default: false [DEPRECATED: instead, configure csp_report_only] + report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives @@ -69,6 +69,7 @@ SecureHeaders::Configuration.default do |config| upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) } + # This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below. config.csp_report_only = config.csp.merge({ img_src: %w(somewhereelse.com), report_uri: %w(https://report-uri.io/example-csp-report-only) From 50eac98be105307edfa9f86b7f0d948f2eccb058 Mon Sep 17 00:00:00 2001 From: Lalith Rallabhandi Date: Wed, 28 Sep 2016 23:51:00 -0400 Subject: [PATCH 305/636] Fix readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 3d951f33..feeedb58 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,6 @@ Configuration.default do |config| end ``` -If ** - ## Named Appends Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. From e75af9ff86b0525ba9aa4521dad54ca5f848f15a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 29 Sep 2016 08:00:53 -1000 Subject: [PATCH 306/636] restrict term-ansicolor so it works on 1.9 --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 543d82cc..77b09935 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ group :test do gem "rack", "~> 1" gem "rspec" gem "coveralls" + gem "term-ansicolor", "< 1.4" end group :guard do From f543be044f5630b10591bee7ffe23c695f6a0e8c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 29 Sep 2016 08:07:39 -1000 Subject: [PATCH 307/636] Remove "use" section because everyone knows how to do this and would use bundler anyways. --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index feeedb58..339c0fa1 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,6 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings. -## Use - -`gem install secure_headers` - ## Configuration If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. From af338e82d89db7e35f5daee9c067e41185f32709 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 29 Sep 2016 10:50:22 -1000 Subject: [PATCH 308/636] safari 10 supports nonces --- .../headers/content_security_policy.rb | 4 +++- spec/lib/secure_headers_spec.rb | 14 ++++++++++++++ spec/spec_helper.rb | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index a7242cc5..64547728 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -8,6 +8,7 @@ class ContentSecurityPolicy # constants to be used for version-specific UA sniffing VERSION_46 = ::UserAgent::Version.new("46") + VERSION_10 = ::UserAgent::Version.new("10") def initialize(config = nil, user_agent = OTHER) @config = if config.is_a?(Hash) @@ -223,7 +224,8 @@ def supported_directives end def nonces_supported? - @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser) + @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser) || + @parsed_ua.browser == "Safari" && @parsed_ua.version >= VERSION_10 end def symbol_to_hyphen_case(sym) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 30279e5e..5715d838 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -277,6 +277,20 @@ module SecureHeaders expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") end + it "uses a nonce for safari 10+" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w(mycdn.com) + } + end + + safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10])) + nonce = SecureHeaders.content_security_policy_script_nonce(safari_request) + hash = SecureHeaders.header_hash_for(safari_request) + expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'") + end + it "supports the deprecated `report_only: true` format" do expect(Kernel).to receive(:warn).once diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 20e8eb82..6842c9bc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,7 +21,8 @@ ios6: "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25", safari5: "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3", safari5_1: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", - safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1" + safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1", + safari10: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.11 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.11" } def expect_default_values(hash) From 6f0c453411042ed5093b0957d756cca6ff5a6586 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 29 Sep 2016 10:54:29 -1000 Subject: [PATCH 309/636] add content_security_policy_script/style_nonce to view helper --- lib/secure_headers/view_helper.rb | 8 ++++++++ spec/lib/secure_headers/view_helpers_spec.rb | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 049f578c..d5ceb329 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -34,6 +34,14 @@ def content_security_policy_nonce(type) end end + def content_security_policy_script_nonce + content_security_policy_nonce(:script) + end + + def content_security_policy_style_nonce + content_security_policy_nonce(:style) + end + ## # Checks to see if the hashed code is expected and adds the hash source # value to the current CSP. diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 17723853..5e25470b 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -27,6 +27,16 @@ def self.template background-color: black; } <% end %> + + + + <%= @name %> TEMPLATE From 680e7e9de740de92b3d88f06cf663529fce7d144 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 29 Sep 2016 11:52:11 -1000 Subject: [PATCH 310/636] make the nonce support helper method public --- lib/secure_headers.rb | 1 + lib/secure_headers/headers/content_security_policy.rb | 3 +-- lib/secure_headers/headers/policy_management.rb | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index a5112ffe..051d5296 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -47,6 +47,7 @@ def opt_out? SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze HTTPS = "https".freeze + CSP = ContentSecurityPolicy ALL_HEADER_CLASSES = [ ContentSecurityPolicyConfig, diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 64547728..65fad480 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -224,8 +224,7 @@ def supported_directives end def nonces_supported? - @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser) || - @parsed_ua.browser == "Safari" && @parsed_ua.version >= VERSION_10 + @nonces_supported ||= self.class.nonces_supported?(@parsed_ua) end def symbol_to_hyphen_case(sym) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index e52eda3a..033861a9 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -211,6 +211,15 @@ def validate_config!(config) end end + # Public: check if a user agent supports CSP nonces + # + # user_agent - a String or a UserAgent object + def nonces_supported?(user_agent) + user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String) + MODERN_BROWSERS.include?(user_agent.browser) || + user_agent.browser == "Safari" && user_agent.version >= CSP::VERSION_10 + end + # Public: combine the values from two different configs. # # original - the main config From e98d8f1814f1d369fc9869a58890959f50b38af5 Mon Sep 17 00:00:00 2001 From: Ryan Caught Date: Tue, 4 Oct 2016 22:04:07 +1000 Subject: [PATCH 311/636] Attempt at breaking up the documentation --- README.md | 472 +--------------------------- docs/HPKP.md | 17 + docs/README.md | 13 + docs/configuration.md | 62 ++++ docs/cookies.md | 50 +++ docs/defaults.md | 26 ++ docs/hashes.md | 35 +++ docs/helpers.md | 28 ++ docs/named_overrides_and_appends.md | 107 +++++++ docs/per_action_configuration.md | 105 +++++++ docs/rails.md | 8 + docs/sinatra.md | 25 ++ 12 files changed, 478 insertions(+), 470 deletions(-) create mode 100644 docs/HPKP.md create mode 100644 docs/README.md create mode 100644 docs/configuration.md create mode 100644 docs/cookies.md create mode 100644 docs/defaults.md create mode 100644 docs/hashes.md create mode 100644 docs/helpers.md create mode 100644 docs/named_overrides_and_appends.md create mode 100644 docs/per_action_configuration.md create mode 100644 docs/rails.md create mode 100644 docs/sinatra.md diff --git a/README.md b/README.md index 339c0fa1..2209a123 100644 --- a/README.md +++ b/README.md @@ -20,477 +20,9 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings. -## Configuration +## Documentation -If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. - -All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. - -```ruby -SecureHeaders::Configuration.default do |config| - config.cookies = { - secure: true, # mark all cookies as "Secure" - httponly: true, # mark all cookies as "HttpOnly" - samesite: { - lax: true # mark all cookies as SameSite=lax - } - } - config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" - config.x_frame_options = "DENY" - config.x_content_type_options = "nosniff" - config.x_xss_protection = "1; mode=block" - config.x_download_options = "noopen" - config.x_permitted_cross_domain_policies = "none" - config.referrer_policy = "origin-when-cross-origin" - config.csp = { - # "meta" values. these will shaped the header, but the values are not included in the header. - report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] - preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. - - # directive values: these values will directly translate into source directives - default_src: %w(https: 'self'), - base_uri: %w('self'), - block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ - child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. - connect_src: %w(wss:), - font_src: %w('self' data:), - form_action: %w('self' github.com), - frame_ancestors: %w('none'), - img_src: %w(mycdn.com data:), - media_src: %w(utoob.com), - object_src: %w('self'), - plugin_types: %w(application/x-shockwave-flash), - script_src: %w('self'), - style_src: %w('unsafe-inline'), - upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ - report_uri: %w(https://report-uri.io/example-csp) - } - # This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below. - config.csp_report_only = config.csp.merge({ - img_src: %w(somewhereelse.com), - report_uri: %w(https://report-uri.io/example-csp-report-only) - }) - config.hpkp = { - report_only: false, - max_age: 60.days.to_i, - include_subdomains: true, - report_uri: "https://report-uri.io/example-hpkp", - pins: [ - {sha256: "abc"}, - {sha256: "123"} - ] - } -end -``` - -### rails 2 - -For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 or non-rails applications, an explicit statement is required to use the middleware component. - -```ruby -use SecureHeaders::Middleware -``` - -## Default values - -All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is: - -``` -Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' -Strict-Transport-Security: max-age=631138519 -X-Content-Type-Options: nosniff -X-Download-Options: noopen -X-Frame-Options: sameorigin -X-Permitted-Cross-Domain-Policies: none -X-Xss-Protection: 1; mode=block -``` - -### Default CSP - -By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. - -```ruby -Configuration.default do |config| - config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. - config.csp_report_only = { - default_src: %w('self') - } -end -``` - -## Named Appends - -Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. - -```ruby -def show - if include_widget? - @widget = widget.render - use_content_security_policy_named_append(:widget_partial) - end -end - - -SecureHeaders::Configuration.named_append(:widget_partial) do |request| - SecureHeaders.override_x_frame_options(request, "DENY") - if request.controller_instance.current_user.in_test_bucket? - { child_src: %w(beta.thirdpartyhost.com) } - else - { child_src: %w(thirdpartyhost.com) } - end -end -``` - -You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following: - -```ruby -SecureHeader::Configuration.default do |config| - config.csp = { default_src: %w('self')} -end - -SecureHeaders::Configuration.named_append(:A) do |request| - { default_src: %w(myhost.com) } -end - -SecureHeaders::Configuration.named_append(:B) do |request| - { script_src: %w('unsafe-eval') } -end -``` - -The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.): - -```ruby -def index - use_content_security_policy_named_append(:A) - use_content_security_policy_named_append(:B) - # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval'; -end - -def show - use_content_security_policy_named_append(:B) - use_content_security_policy_named_append(:A) - # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval'; -end -``` - - -## Named overrides - -Named overrides serve two purposes: - -* To be able to refer to a configuration by simple name. -* By precomputing the headers for a named configuration, the headers generated once and reused over every request. - -To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override. - -```ruby -class ApplicationController < ActionController::Base - SecureHeaders::Configuration.default do |config| - config.csp = { - default_src: %w('self'), - script_src: %w(example.org) - } - end - - # override default configuration - SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config| - config.csp[:script_src] << "otherdomain.com" - end - - # overrides the :script_from_otherdomain_com configuration - SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config| - config.csp[:script_src] << "evenanotherdomain.com" - end -end - -class MyController < ApplicationController - def index - # Produces default-src 'self'; script-src example.org otherdomain.com - use_secure_headers_override(:script_from_otherdomain_com) - end - - def show - # Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com - use_secure_headers_override(:another_config) - end -end -``` - -By default, a no-op configuration is provided. No headers will be set when this default override is used. - -```ruby -class MyController < ApplicationController - def index - SecureHeaders.opt_out_of_all_protection(request) - end -end -``` - -## Per-action configuration - -You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request. - -```ruby -# Given a config of: -::SecureHeaders::Configuration.default do |config| - config.csp = { - default_src: %w('self'), - script_src: %w('self') - } - end - -class MyController < ApplicationController - def index - # Append value to the source list, override 'none' values - # Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com - append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com)) - - # Overrides the previously set source list, override 'none' values - # Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self' - override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self')) - - # Global settings default to "sameorigin" - override_x_frame_options("DENY") - end -``` - -The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object. -* `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration. -* `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config -* `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value` - -## Appending / overriding Content Security Policy - -When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`. - -#### Append to the policy with a directive other than `default_src` - -The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate: - -```ruby -::SecureHeaders::Configuration.default do |config| - config.csp = { - default_src: %w('self') - } - end - ``` - -Code | Result -------------- | ------------- -`append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com` -`override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com` - -#### Nonce - -You can use a view helper to automatically add nonces to script tags: - -```erb -<%= nonced_javascript_tag do %> -console.log("nonced!"); -<% end %> - -<%= nonced_style_tag do %> -body { - background-color: black; -} -<% end %> -``` - -becomes: - -```html - - -``` - -``` - -Content-Security-Policy: ... - script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; - style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; -``` - -`script`/`style-nonce` can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags. - -```erb - - - - - -``` - -#### Hash - -`script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. - -You can add hash sources directly to your policy : - -```ruby -::SecureHeaders::Configuration.default do |config| - config.csp = { - default_src: %w('self') - - # this is a made up value but browsers will show the expected hash in the console. - script_src: %w(sha256-123456) - } - end - ``` - - You can also use the automated inline script detection/collection/computation of hash source values in your app. - - ```bash - rake secure_headers:generate_hashes - ``` - - This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. - -```yaml ---- -scripts: - app/views/asdfs/index.html.erb: - - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='" -styles: - app/views/asdfs/index.html.erb: - - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" - - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" -``` - -##### Helpers - -**This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. - -```erb -<%= hashed_style_tag do %> -body { - background-color: black; -} -<% end %> - -<%= hashed_style_tag do %> -body { - font-size: 30px; - font-color: green; -} -<% end %> - -<%= hashed_javascript_tag do %> -console.log(1) -<% end %> -``` - -``` -Content-Security-Policy: ... - script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; - style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; -``` - -### Public Key Pins - -Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work. - -```ruby -config.hpkp = { - max_age: 60.days.to_i, # max_age is a required parameter - include_subdomains: true, # whether or not to apply pins to subdomains - # Per the spec, SHA256 hashes are the only currently supported format. - pins: [ - {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, - {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} - ], - report_only: true, # defaults to false (report-only mode) - report_uri: 'https://report-uri.io/example-hpkp' -} -``` - -### Cookies - -SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. - -__Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. - -#### Boolean-based configuration - -Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. - -```ruby -config.cookies = { - secure: true, # mark all cookies as Secure - httponly: false, # do not mark any cookies as HttpOnly -} -``` - -#### Hash-based configuration - -Hash-based configuration allows for fine-grained control. - -```ruby -config.cookies = { - secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure - httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly -} -``` - -#### SameSite cookie configuration - -SameSite cookies permit either `Strict` or `Lax` enforcement mode options. - -```ruby -config.cookies = { - samesite: { - strict: true # mark all cookies as SameSite=Strict - } -} -``` - -`Strict` and `Lax` enforcement modes can also be specified using a Hash. - -```ruby -config.cookies = { - samesite: { - strict: { only: ['_rails_session'] }, - lax: { only: ['_guest'] } - } -} -``` - -### Using with Sinatra - -Here's an example using SecureHeaders for Sinatra applications: - -```ruby -require 'rubygems' -require 'sinatra' -require 'haml' -require 'secure_headers' - -use SecureHeaders::Middleware - -SecureHeaders::Configuration.default do |config| - ... -end - -class Donkey < Sinatra::Application - set :root, APP_ROOT - - get '/' do - SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT) - haml :index - end -end -``` +[Find the latest documention here.](docs/README.md) ## Similar libraries diff --git a/docs/HPKP.md b/docs/HPKP.md new file mode 100644 index 00000000..33d78085 --- /dev/null +++ b/docs/HPKP.md @@ -0,0 +1,17 @@ +### Public Key Pins + +Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work. + +```ruby +config.hpkp = { + max_age: 60.days.to_i, # max_age is a required parameter + include_subdomains: true, # whether or not to apply pins to subdomains + # Per the spec, SHA256 hashes are the only currently supported format. + pins: [ + {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, + {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} + ], + report_only: true, # defaults to false (report-only mode) + report_uri: 'https://report-uri.io/example-hpkp' +} +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..17001652 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,13 @@ +# Documentation + + +- [Configuration](configuration.md) + - [Defaults](defaults.md) + - [Named overrides and appends](named_overrides_and_appends.md) + - [Per action configuration](per_action_configuration.md) +- Frameworks + - [Rails 2](rails.md) + - [Sinatra](sinatra.md) +- [Cookies](cookies.md) +- [HPKP](HPKP.md) +- [Hashes](hashes.md) diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..e620acc8 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,62 @@ +## Configuration + +If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. + +All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. + +```ruby +SecureHeaders::Configuration.default do |config| + config.cookies = { + secure: true, # mark all cookies as "Secure" + httponly: true, # mark all cookies as "HttpOnly" + samesite: { + lax: true # mark all cookies as SameSite=lax + } + } + config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" + config.x_frame_options = "DENY" + config.x_content_type_options = "nosniff" + config.x_xss_protection = "1; mode=block" + config.x_download_options = "noopen" + config.x_permitted_cross_domain_policies = "none" + config.referrer_policy = "origin-when-cross-origin" + config.csp = { + # "meta" values. these will shaped the header, but the values are not included in the header. + report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] + preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. + + # directive values: these values will directly translate into source directives + default_src: %w(https: 'self'), + base_uri: %w('self'), + block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ + child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. + connect_src: %w(wss:), + font_src: %w('self' data:), + form_action: %w('self' github.com), + frame_ancestors: %w('none'), + img_src: %w(mycdn.com data:), + media_src: %w(utoob.com), + object_src: %w('self'), + plugin_types: %w(application/x-shockwave-flash), + script_src: %w('self'), + style_src: %w('unsafe-inline'), + upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ + report_uri: %w(https://report-uri.io/example-csp) + } + # This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below. + config.csp_report_only = config.csp.merge({ + img_src: %w(somewhereelse.com), + report_uri: %w(https://report-uri.io/example-csp-report-only) + }) + config.hpkp = { + report_only: false, + max_age: 60.days.to_i, + include_subdomains: true, + report_uri: "https://report-uri.io/example-hpkp", + pins: [ + {sha256: "abc"}, + {sha256: "123"} + ] + } +end +``` diff --git a/docs/cookies.md b/docs/cookies.md new file mode 100644 index 00000000..570d8326 --- /dev/null +++ b/docs/cookies.md @@ -0,0 +1,50 @@ +### Cookies + +SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. + +__Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. + +#### Boolean-based configuration + +Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. + +```ruby +config.cookies = { + secure: true, # mark all cookies as Secure + httponly: false, # do not mark any cookies as HttpOnly +} +``` + +#### Hash-based configuration + +Hash-based configuration allows for fine-grained control. + +```ruby +config.cookies = { + secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure + httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly +} +``` + +#### SameSite cookie configuration + +SameSite cookies permit either `Strict` or `Lax` enforcement mode options. + +```ruby +config.cookies = { + samesite: { + strict: true # mark all cookies as SameSite=Strict + } +} +``` + +`Strict` and `Lax` enforcement modes can also be specified using a Hash. + +```ruby +config.cookies = { + samesite: { + strict: { only: ['_rails_session'] }, + lax: { only: ['_guest'] } + } +} +``` diff --git a/docs/defaults.md b/docs/defaults.md new file mode 100644 index 00000000..ab07f6c8 --- /dev/null +++ b/docs/defaults.md @@ -0,0 +1,26 @@ +## Default values + +All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is: + +``` +Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' +Strict-Transport-Security: max-age=631138519 +X-Content-Type-Options: nosniff +X-Download-Options: noopen +X-Frame-Options: sameorigin +X-Permitted-Cross-Domain-Policies: none +X-Xss-Protection: 1; mode=block +``` + +### Default CSP + +By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. + +```ruby +Configuration.default do |config| + config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. + config.csp_report_only = { + default_src: %w('self') + } +end +``` diff --git a/docs/hashes.md b/docs/hashes.md new file mode 100644 index 00000000..de2882af --- /dev/null +++ b/docs/hashes.md @@ -0,0 +1,35 @@ +#### Hash + +`script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. + +You can add hash sources directly to your policy : + +```ruby +::SecureHeaders::Configuration.default do |config| + config.csp = { + default_src: %w('self') + + # this is a made up value but browsers will show the expected hash in the console. + script_src: %w(sha256-123456) + } + end + ``` + + You can also use the automated inline script detection/collection/computation of hash source values in your app. + + ```bash + rake secure_headers:generate_hashes + ``` + + This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. + +```yaml +--- +scripts: + app/views/asdfs/index.html.erb: + - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='" +styles: + app/views/asdfs/index.html.erb: + - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" + - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" +``` diff --git a/docs/helpers.md b/docs/helpers.md new file mode 100644 index 00000000..7de58630 --- /dev/null +++ b/docs/helpers.md @@ -0,0 +1,28 @@ +##### Helpers + +**This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. + +```erb +<%= hashed_style_tag do %> +body { + background-color: black; +} +<% end %> + +<%= hashed_style_tag do %> +body { + font-size: 30px; + font-color: green; +} +<% end %> + +<%= hashed_javascript_tag do %> +console.log(1) +<% end %> +``` + +``` +Content-Security-Policy: ... + script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; + style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; +``` diff --git a/docs/named_overrides_and_appends.md b/docs/named_overrides_and_appends.md new file mode 100644 index 00000000..5cb374c4 --- /dev/null +++ b/docs/named_overrides_and_appends.md @@ -0,0 +1,107 @@ +## Named Appends + +Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. + +```ruby +def show + if include_widget? + @widget = widget.render + use_content_security_policy_named_append(:widget_partial) + end +end + + +SecureHeaders::Configuration.named_append(:widget_partial) do |request| + SecureHeaders.override_x_frame_options(request, "DENY") + if request.controller_instance.current_user.in_test_bucket? + { child_src: %w(beta.thirdpartyhost.com) } + else + { child_src: %w(thirdpartyhost.com) } + end +end +``` + +You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following: + +```ruby +SecureHeader::Configuration.default do |config| + config.csp = { default_src: %w('self')} +end + +SecureHeaders::Configuration.named_append(:A) do |request| + { default_src: %w(myhost.com) } +end + +SecureHeaders::Configuration.named_append(:B) do |request| + { script_src: %w('unsafe-eval') } +end +``` + +The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.): + +```ruby +def index + use_content_security_policy_named_append(:A) + use_content_security_policy_named_append(:B) + # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval'; +end + +def show + use_content_security_policy_named_append(:B) + use_content_security_policy_named_append(:A) + # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval'; +end +``` + + +## Named overrides + +Named overrides serve two purposes: + +* To be able to refer to a configuration by simple name. +* By precomputing the headers for a named configuration, the headers generated once and reused over every request. + +To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override. + +```ruby +class ApplicationController < ActionController::Base + SecureHeaders::Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w(example.org) + } + end + + # override default configuration + SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config| + config.csp[:script_src] << "otherdomain.com" + end + + # overrides the :script_from_otherdomain_com configuration + SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config| + config.csp[:script_src] << "evenanotherdomain.com" + end +end + +class MyController < ApplicationController + def index + # Produces default-src 'self'; script-src example.org otherdomain.com + use_secure_headers_override(:script_from_otherdomain_com) + end + + def show + # Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com + use_secure_headers_override(:another_config) + end +end +``` + +By default, a no-op configuration is provided. No headers will be set when this default override is used. + +```ruby +class MyController < ApplicationController + def index + SecureHeaders.opt_out_of_all_protection(request) + end +end +``` diff --git a/docs/per_action_configuration.md b/docs/per_action_configuration.md new file mode 100644 index 00000000..95799313 --- /dev/null +++ b/docs/per_action_configuration.md @@ -0,0 +1,105 @@ +## Per-action configuration + +You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request. + +```ruby +# Given a config of: +::SecureHeaders::Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self') + } + end + +class MyController < ApplicationController + def index + # Append value to the source list, override 'none' values + # Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com + append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com)) + + # Overrides the previously set source list, override 'none' values + # Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self' + override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self')) + + # Global settings default to "sameorigin" + override_x_frame_options("DENY") + end +``` + +The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object. +* `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration. +* `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config +* `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value` + +## Appending / overriding Content Security Policy + +When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`. + +#### Append to the policy with a directive other than `default_src` + +The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate: + +```ruby +::SecureHeaders::Configuration.default do |config| + config.csp = { + default_src: %w('self') + } + end + ``` + +Code | Result +------------- | ------------- +`append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com` +`override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com` + +#### Nonce + +You can use a view helper to automatically add nonces to script tags: + +```erb +<%= nonced_javascript_tag do %> +console.log("nonced!"); +<% end %> + +<%= nonced_style_tag do %> +body { + background-color: black; +} +<% end %> +``` + +becomes: + +```html + + +``` + +``` + +Content-Security-Policy: ... + script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; + style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; +``` + +`script`/`style-nonce` can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags. + +```erb + + + + + +``` diff --git a/docs/rails.md b/docs/rails.md new file mode 100644 index 00000000..524ebe0b --- /dev/null +++ b/docs/rails.md @@ -0,0 +1,8 @@ +### rails 2 + +For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 or non-rails applications, an explicit statement is required to use the middleware component. + +```ruby +use SecureHeaders::Middleware +``` + diff --git a/docs/sinatra.md b/docs/sinatra.md new file mode 100644 index 00000000..5f0f2ee5 --- /dev/null +++ b/docs/sinatra.md @@ -0,0 +1,25 @@ +### Using with Sinatra + +Here's an example using SecureHeaders for Sinatra applications: + +```ruby +require 'rubygems' +require 'sinatra' +require 'haml' +require 'secure_headers' + +use SecureHeaders::Middleware + +SecureHeaders::Configuration.default do |config| + ... +end + +class Donkey < Sinatra::Application + set :root, APP_ROOT + + get '/' do + SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT) + haml :index + end +end +``` From 53f0d3ddbae5ae5ba4b109f076f0b89a5cea55cb Mon Sep 17 00:00:00 2001 From: Ryan Caught Date: Wed, 5 Oct 2016 21:42:48 +1000 Subject: [PATCH 312/636] Improvements to the documentation Changes suggested by @oreoshake and @jacobbednarz. --- README.md | 8 ++++++- docs/HPKP.md | 2 +- docs/README.md | 13 ----------- docs/configuration.md | 27 +++++++++++++++++++++ docs/cookies.md | 2 +- docs/defaults.md | 26 --------------------- docs/{sinatra.md => getting_started.md} | 25 +++++++++++++++++++- docs/hashes.md | 31 ++++++++++++++++++++++++- docs/helpers.md | 28 ---------------------- docs/rails.md | 8 ------- 10 files changed, 90 insertions(+), 80 deletions(-) delete mode 100644 docs/README.md delete mode 100644 docs/defaults.md rename docs/{sinatra.md => getting_started.md} (50%) delete mode 100644 docs/helpers.md delete mode 100644 docs/rails.md diff --git a/README.md b/README.md index 2209a123..26a1f92f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,13 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu ## Documentation -[Find the latest documention here.](docs/README.md) +- [Getting Started](getting_started.md) +- [Base Configuration](configuration.md) +- [Named overrides and appends](named_overrides_and_appends.md) +- [Per action configuration](per_action_configuration.md) +- [Cookies](cookies.md) +- [HPKP](HPKP.md) +- [Hashes](hashes.md) ## Similar libraries diff --git a/docs/HPKP.md b/docs/HPKP.md index 33d78085..9428d0d9 100644 --- a/docs/HPKP.md +++ b/docs/HPKP.md @@ -1,4 +1,4 @@ -### Public Key Pins +## HTTP Public Key Pins Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work. diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 17001652..00000000 --- a/docs/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Documentation - - -- [Configuration](configuration.md) - - [Defaults](defaults.md) - - [Named overrides and appends](named_overrides_and_appends.md) - - [Per action configuration](per_action_configuration.md) -- Frameworks - - [Rails 2](rails.md) - - [Sinatra](sinatra.md) -- [Cookies](cookies.md) -- [HPKP](HPKP.md) -- [Hashes](hashes.md) diff --git a/docs/configuration.md b/docs/configuration.md index e620acc8..fa58aab4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -60,3 +60,30 @@ SecureHeaders::Configuration.default do |config| } end ``` + +## Default values + +All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is: + +``` +Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' +Strict-Transport-Security: max-age=631138519 +X-Content-Type-Options: nosniff +X-Download-Options: noopen +X-Frame-Options: sameorigin +X-Permitted-Cross-Domain-Policies: none +X-Xss-Protection: 1; mode=block +``` + +### Default CSP + +By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. + +```ruby +Configuration.default do |config| + config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. + config.csp_report_only = { + default_src: %w('self') + } +end +``` diff --git a/docs/cookies.md b/docs/cookies.md index 570d8326..1aff0125 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -1,4 +1,4 @@ -### Cookies +## Cookies SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. diff --git a/docs/defaults.md b/docs/defaults.md deleted file mode 100644 index ab07f6c8..00000000 --- a/docs/defaults.md +++ /dev/null @@ -1,26 +0,0 @@ -## Default values - -All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is: - -``` -Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' -Strict-Transport-Security: max-age=631138519 -X-Content-Type-Options: nosniff -X-Download-Options: noopen -X-Frame-Options: sameorigin -X-Permitted-Cross-Domain-Policies: none -X-Xss-Protection: 1; mode=block -``` - -### Default CSP - -By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. - -```ruby -Configuration.default do |config| - config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. - config.csp_report_only = { - default_src: %w('self') - } -end -``` diff --git a/docs/sinatra.md b/docs/getting_started.md similarity index 50% rename from docs/sinatra.md rename to docs/getting_started.md index 5f0f2ee5..1fb39606 100644 --- a/docs/sinatra.md +++ b/docs/getting_started.md @@ -1,4 +1,27 @@ -### Using with Sinatra +## Getting Started + +Add the gem to your `Gemfile` and `bundle install`. + +``` +gem 'secure_headers' +``` + +## Rails + +### Rails 3+ + +For Rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. + +### Rails 2 + +For Rails 2 or non-rails applications, an explicit statement is required to use the middleware component. + +```ruby +use SecureHeaders::Middleware +``` + + +## Sinatra Here's an example using SecureHeaders for Sinatra applications: diff --git a/docs/hashes.md b/docs/hashes.md index de2882af..b917a204 100644 --- a/docs/hashes.md +++ b/docs/hashes.md @@ -1,4 +1,4 @@ -#### Hash +## Hash `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. @@ -33,3 +33,32 @@ styles: - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" ``` + +##### Helpers + +**This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. + +```erb +<%= hashed_style_tag do %> +body { + background-color: black; +} +<% end %> + +<%= hashed_style_tag do %> +body { + font-size: 30px; + font-color: green; +} +<% end %> + +<%= hashed_javascript_tag do %> +console.log(1) +<% end %> +``` + +``` +Content-Security-Policy: ... + script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; + style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; +``` diff --git a/docs/helpers.md b/docs/helpers.md deleted file mode 100644 index 7de58630..00000000 --- a/docs/helpers.md +++ /dev/null @@ -1,28 +0,0 @@ -##### Helpers - -**This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. - -```erb -<%= hashed_style_tag do %> -body { - background-color: black; -} -<% end %> - -<%= hashed_style_tag do %> -body { - font-size: 30px; - font-color: green; -} -<% end %> - -<%= hashed_javascript_tag do %> -console.log(1) -<% end %> -``` - -``` -Content-Security-Policy: ... - script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; - style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; -``` diff --git a/docs/rails.md b/docs/rails.md deleted file mode 100644 index 524ebe0b..00000000 --- a/docs/rails.md +++ /dev/null @@ -1,8 +0,0 @@ -### rails 2 - -For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 or non-rails applications, an explicit statement is required to use the middleware component. - -```ruby -use SecureHeaders::Middleware -``` - From 57e77b73d69b8b11608194067a6bdeff215bde42 Mon Sep 17 00:00:00 2001 From: Anuj Das Date: Tue, 11 Oct 2016 17:04:37 -0700 Subject: [PATCH 313/636] Set secure cookies on interleaved http/https calls correctly The current HTTP/HTTPS detection code mutates the cookie config in place in order to switch between secure and non-secure cookies. However, since the original global secure_headers config was passed around everywhere, the very first HTTP request received would modify the global cookie config, resulting in :secure => false for all subsequent requests regardless of SSL. The solution is to dup the cookie config before using it, which is a pattern followed for every other configuration type anyways; a better long-term fix would be using non-mutating methods instead. --- .ruby-version | 2 +- lib/secure_headers/configuration.rb | 2 +- spec/lib/secure_headers/middleware_spec.rb | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.ruby-version b/.ruby-version index 58594069..21bb5e15 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.2.3 +2.2.5 diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index bf459ae3..b0a53df2 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -140,7 +140,7 @@ def initialize(&block) # Returns a deep-dup'd copy of this configuration. def dup copy = self.class.new - copy.cookies = @cookies + copy.cookies = self.class.send(:deep_copy_if_hash, @cookies) copy.csp = @csp.dup if @csp copy.csp_report_only = @csp_report_only.dup if @csp_report_only copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 13fbf8c9..bfd78ae8 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -105,6 +105,18 @@ module SecureHeaders _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to eq("foo=bar") end + + it "sets the secure cookie flag correctly on interleaved http/https requests" do + Configuration.default { |config| config.cookies = { secure: true } } + + request = Rack::Request.new("HTTPS" => "off") + _, env = cookie_middleware.call request.env + expect(env['Set-Cookie']).to eq("foo=bar") + + request = Rack::Request.new("HTTPS" => "on") + _, env = cookie_middleware.call request.env + expect(env['Set-Cookie']).to eq("foo=bar; secure") + end end end end From 85760fe6ad19cc8ae723a4b95bd13792c200d17d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 12 Oct 2016 07:21:20 -1000 Subject: [PATCH 314/636] bump to v3.5.0 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 4f2b9376..2c09c8a8 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.5.0.pre" + gem.version = "3.5.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 04f42abab0c7cb617d99165239eb44b7b2aaf2fa Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 12 Oct 2016 07:48:02 -1000 Subject: [PATCH 315/636] update changelog tag --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 457f470b..cf2e50f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -## 3.5.0.pre +## 3.5.0 This release adds support for setting two CSP headers (enforced/report-only) and management around them. + ## 3.4.1 Named Appends ### Small bugfix From cd66deeba6b39b33a88c087de150e4f0c7793804 Mon Sep 17 00:00:00 2001 From: Jeremy Durham Date: Tue, 25 Oct 2016 01:07:35 -0400 Subject: [PATCH 316/636] Add missing Referrer Policies * Add same-origin, strict-origin and strict-origin-when-cross-origin --- lib/secure_headers/headers/referrer_policy.rb | 3 +++ .../headers/referrer_policy_spec.rb | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index 7f433aa9..63898b6f 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -6,6 +6,9 @@ class ReferrerPolicy VALID_POLICIES = %w( no-referrer no-referrer-when-downgrade + same-origin + strict-origin + strict-origin-when-cross-origin origin origin-when-cross-origin unsafe-url diff --git a/spec/lib/secure_headers/headers/referrer_policy_spec.rb b/spec/lib/secure_headers/headers/referrer_policy_spec.rb index a3a9f6dd..35f9f824 100644 --- a/spec/lib/secure_headers/headers/referrer_policy_spec.rb +++ b/spec/lib/secure_headers/headers/referrer_policy_spec.rb @@ -18,6 +18,24 @@ module SecureHeaders end.not_to raise_error end + it "accepts 'same-origin'" do + expect do + ReferrerPolicy.validate_config!("same-origin") + end.not_to raise_error + end + + it "accepts 'strict-origin'" do + expect do + ReferrerPolicy.validate_config!("strict-origin") + end.not_to raise_error + end + + it "accepts 'strict-origin-when-cross-origin'" do + expect do + ReferrerPolicy.validate_config!("strict-origin-when-cross-origin") + end.not_to raise_error + end + it "accepts 'origin'" do expect do ReferrerPolicy.validate_config!("origin") From c22259786ac65af2c06bc9c4deb6be0b8dddc388 Mon Sep 17 00:00:00 2001 From: stve Date: Mon, 7 Nov 2016 17:00:47 -0500 Subject: [PATCH 317/636] handle nil useragent version --- lib/secure_headers/headers/content_security_policy.rb | 3 ++- lib/secure_headers/headers/policy_management.rb | 2 +- .../secure_headers/headers/content_security_policy_spec.rb | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 65fad480..44ce1c72 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -9,6 +9,7 @@ class ContentSecurityPolicy # constants to be used for version-specific UA sniffing VERSION_46 = ::UserAgent::Version.new("46") VERSION_10 = ::UserAgent::Version.new("10") + FALLBACK_VERSION = ::UserAgent::Version.new("0") def initialize(config = nil, user_agent = OTHER) @config = if config.is_a?(Hash) @@ -213,7 +214,7 @@ def strip_source_schemes(source_list) # Returns an array of symbols representing the directives. def supported_directives @supported_directives ||= if VARIATIONS[@parsed_ua.browser] - if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46 + if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46) VARIATIONS["FirefoxTransitional"] else VARIATIONS[@parsed_ua.browser] diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 033861a9..408c86ac 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -217,7 +217,7 @@ def validate_config!(config) def nonces_supported?(user_agent) user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String) MODERN_BROWSERS.include?(user_agent.browser) || - user_agent.browser == "Safari" && user_agent.version >= CSP::VERSION_10 + user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10 end # Public: combine the values from two different configs. diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index ef0fd8ce..01edc3ac 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -149,6 +149,13 @@ module SecureHeaders policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end + + it "falls back to standard Firefox defaults when the useragent version is not present" do + ua = USER_AGENTS[:firefox].dup + allow(ua).to receive(:version).and_return(nil) + policy = ContentSecurityPolicy.new(complex_opts, ua) + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + end end end end From ec4a1c89a5d4baa048414d6e325b510cd5db6329 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 11 Nov 2016 06:21:55 -1000 Subject: [PATCH 318/636] Add google CSP resources /cc @mikispag --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 339c0fa1..5e0ce423 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) + - https://csp.withgoogle.com + - https://csp.withgoogle.com/docs/strict-csp.html + - https://csp-evaluator.withgoogle.com - HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797) - X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034) - X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx) From e8bf322846a910f6c02a3a435795d83f305430c0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 11 Nov 2016 06:34:32 -1000 Subject: [PATCH 319/636] quick and dirty strict-dynamic support' --- lib/secure_headers/headers/policy_management.rb | 1 + .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 408c86ac..20d45753 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -14,6 +14,7 @@ def self.included(base) STAR = "*".freeze UNSAFE_INLINE = "'unsafe-inline'".freeze UNSAFE_EVAL = "'unsafe-eval'".freeze + STRICT_DYNAMIC = "'strict-dynamic'".freeze # leftover deprecated values that will be in common use upon upgrading. DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 01edc3ac..8e43601f 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -107,6 +107,11 @@ module SecureHeaders expect(firefox_transitional).not_to match(/frame-src/) end + it "supports strict-dynamic" do + csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome]) + expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'") + end + context "browser sniffing" do let (:complex_opts) do (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash| From a8531e7edfcba470c94e22f62946122cadc0df52 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 11 Nov 2016 07:04:09 -1000 Subject: [PATCH 320/636] version bump and docs --- CHANGELOG.md | 5 +++++ secure_headers.gemspec | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2e50f5..6f2ce688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.5.1 + +* Fix bug that can occur when useragent library version is older, resulting in a nil version sometimes. +* Add constant for `strict-dynamic` + ## 3.5.0 This release adds support for setting two CSP headers (enforced/report-only) and management around them. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 2c09c8a8..063a65f5 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.5.0" + gem.version = "3.5.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From c6202a9de85e585059c2daa966fbabfea5470a39 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Nov 2016 09:08:17 -1000 Subject: [PATCH 321/636] move content of configuration.md into main README --- README.md | 91 +++++++++++++++++++++++++++++++++++++++++++ docs/configuration.md | 89 ------------------------------------------ 2 files changed, 91 insertions(+), 89 deletions(-) delete mode 100644 docs/configuration.md diff --git a/README.md b/README.md index 579b8a00..4d3117fe 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,97 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu - [HPKP](HPKP.md) - [Hashes](hashes.md) +## Configuration + +If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. + +All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. + +```ruby +SecureHeaders::Configuration.default do |config| + config.cookies = { + secure: true, # mark all cookies as "Secure" + httponly: true, # mark all cookies as "HttpOnly" + samesite: { + lax: true # mark all cookies as SameSite=lax + } + } + config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" + config.x_frame_options = "DENY" + config.x_content_type_options = "nosniff" + config.x_xss_protection = "1; mode=block" + config.x_download_options = "noopen" + config.x_permitted_cross_domain_policies = "none" + config.referrer_policy = "origin-when-cross-origin" + config.csp = { + # "meta" values. these will shaped the header, but the values are not included in the header. + report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] + preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. + + # directive values: these values will directly translate into source directives + default_src: %w(https: 'self'), + base_uri: %w('self'), + block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ + child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. + connect_src: %w(wss:), + font_src: %w('self' data:), + form_action: %w('self' github.com), + frame_ancestors: %w('none'), + img_src: %w(mycdn.com data:), + media_src: %w(utoob.com), + object_src: %w('self'), + plugin_types: %w(application/x-shockwave-flash), + script_src: %w('self'), + style_src: %w('unsafe-inline'), + upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ + report_uri: %w(https://report-uri.io/example-csp) + } + # This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below. + config.csp_report_only = config.csp.merge({ + img_src: %w(somewhereelse.com), + report_uri: %w(https://report-uri.io/example-csp-report-only) + }) + config.hpkp = { + report_only: false, + max_age: 60.days.to_i, + include_subdomains: true, + report_uri: "https://report-uri.io/example-hpkp", + pins: [ + {sha256: "abc"}, + {sha256: "123"} + ] + } +end +``` + +## Default values + +All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is: + +``` +Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' +Strict-Transport-Security: max-age=631138519 +X-Content-Type-Options: nosniff +X-Download-Options: noopen +X-Frame-Options: sameorigin +X-Permitted-Cross-Domain-Policies: none +X-Xss-Protection: 1; mode=block +``` + +### Default CSP + +By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. + +```ruby +Configuration.default do |config| + config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. + config.csp_report_only = { + default_src: %w('self') + } +end +``` + + ## Similar libraries * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers) diff --git a/docs/configuration.md b/docs/configuration.md deleted file mode 100644 index fa58aab4..00000000 --- a/docs/configuration.md +++ /dev/null @@ -1,89 +0,0 @@ -## Configuration - -If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. - -All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. - -```ruby -SecureHeaders::Configuration.default do |config| - config.cookies = { - secure: true, # mark all cookies as "Secure" - httponly: true, # mark all cookies as "HttpOnly" - samesite: { - lax: true # mark all cookies as SameSite=lax - } - } - config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" - config.x_frame_options = "DENY" - config.x_content_type_options = "nosniff" - config.x_xss_protection = "1; mode=block" - config.x_download_options = "noopen" - config.x_permitted_cross_domain_policies = "none" - config.referrer_policy = "origin-when-cross-origin" - config.csp = { - # "meta" values. these will shaped the header, but the values are not included in the header. - report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] - preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. - - # directive values: these values will directly translate into source directives - default_src: %w(https: 'self'), - base_uri: %w('self'), - block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ - child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. - connect_src: %w(wss:), - font_src: %w('self' data:), - form_action: %w('self' github.com), - frame_ancestors: %w('none'), - img_src: %w(mycdn.com data:), - media_src: %w(utoob.com), - object_src: %w('self'), - plugin_types: %w(application/x-shockwave-flash), - script_src: %w('self'), - style_src: %w('unsafe-inline'), - upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ - report_uri: %w(https://report-uri.io/example-csp) - } - # This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below. - config.csp_report_only = config.csp.merge({ - img_src: %w(somewhereelse.com), - report_uri: %w(https://report-uri.io/example-csp-report-only) - }) - config.hpkp = { - report_only: false, - max_age: 60.days.to_i, - include_subdomains: true, - report_uri: "https://report-uri.io/example-hpkp", - pins: [ - {sha256: "abc"}, - {sha256: "123"} - ] - } -end -``` - -## Default values - -All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is: - -``` -Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' -Strict-Transport-Security: max-age=631138519 -X-Content-Type-Options: nosniff -X-Download-Options: noopen -X-Frame-Options: sameorigin -X-Permitted-Cross-Domain-Policies: none -X-Xss-Protection: 1; mode=block -``` - -### Default CSP - -By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. - -```ruby -Configuration.default do |config| - config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. - config.csp_report_only = { - default_src: %w('self') - } -end -``` From 96ceea6790bff63082809b067c0b4618889ac6f3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Nov 2016 09:09:04 -1000 Subject: [PATCH 322/636] link the README as the docs index file --- docs/index.md | 1 + 1 file changed, 1 insertion(+) create mode 120000 docs/index.md diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 00000000..42061c01 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +README.md \ No newline at end of file From d8591444d031ddf89d73f78b1a9ba22e930859c4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Nov 2016 09:13:20 -1000 Subject: [PATCH 323/636] add more to the main readme, make the sinatra config a standalone file --- README.md | 19 ++++++++++++++++--- docs/{getting_started.md => sinatra.md} | 23 ----------------------- 2 files changed, 16 insertions(+), 26 deletions(-) rename docs/{getting_started.md => sinatra.md} (51%) diff --git a/README.md b/README.md index 4d3117fe..59adaf17 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,26 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu ## Documentation -- [Getting Started](getting_started.md) -- [Base Configuration](configuration.md) - [Named overrides and appends](named_overrides_and_appends.md) - [Per action configuration](per_action_configuration.md) - [Cookies](cookies.md) - [HPKP](HPKP.md) - [Hashes](hashes.md) +- [Sinatra Config](sinatra.md) + +## Getting Started + +### Rails 3+ + +For Rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. If for some reason the middleware is not being included follow the instructions for Rails 2. + +### Rails 2 + +For Rails 2 or non-rails applications, an explicit statement is required to use the middleware component. + +```ruby +use SecureHeaders::Middleware +``` ## Configuration @@ -98,7 +111,7 @@ end ## Default values -All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is: +All headers except for PublicKeyPins have a default value. The default set of headers is: ``` Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' diff --git a/docs/getting_started.md b/docs/sinatra.md similarity index 51% rename from docs/getting_started.md rename to docs/sinatra.md index 1fb39606..919f7f9c 100644 --- a/docs/getting_started.md +++ b/docs/sinatra.md @@ -1,26 +1,3 @@ -## Getting Started - -Add the gem to your `Gemfile` and `bundle install`. - -``` -gem 'secure_headers' -``` - -## Rails - -### Rails 3+ - -For Rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. - -### Rails 2 - -For Rails 2 or non-rails applications, an explicit statement is required to use the middleware component. - -```ruby -use SecureHeaders::Middleware -``` - - ## Sinatra Here's an example using SecureHeaders for Sinatra applications: From 51d75bff8c2c96f43042ab3392323790ef2d1b1f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Nov 2016 09:16:24 -1000 Subject: [PATCH 324/636] make links to docs anchored at /docs --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 59adaf17..912a5c22 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu ## Documentation -- [Named overrides and appends](named_overrides_and_appends.md) -- [Per action configuration](per_action_configuration.md) -- [Cookies](cookies.md) -- [HPKP](HPKP.md) -- [Hashes](hashes.md) -- [Sinatra Config](sinatra.md) +- [Named overrides and appends](docs/named_overrides_and_appends.md) +- [Per action configuration](docs/per_action_configuration.md) +- [Cookies](docs/cookies.md) +- [HPKP](docs/HPKP.md) +- [Hashes](docs/hashes.md) +- [Sinatra Config](docs/sinatra.md) ## Getting Started From 886305c704fe67a3546ffd1c2d710a03a68e5a82 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 28 Nov 2016 10:03:39 -1000 Subject: [PATCH 325/636] Delete index.md --- docs/index.md | 1 - 1 file changed, 1 deletion(-) delete mode 120000 docs/index.md diff --git a/docs/index.md b/docs/index.md deleted file mode 120000 index 42061c01..00000000 --- a/docs/index.md +++ /dev/null @@ -1 +0,0 @@ -README.md \ No newline at end of file From 9e1ee40d32a988f67d4742e9ed45aad5031a7dc1 Mon Sep 17 00:00:00 2001 From: Benjamin P Toews Date: Wed, 4 Jan 2017 10:42:39 -0700 Subject: [PATCH 326/636] Add support for Clear-Site-Data header --- README.md | 3 +- lib/secure_headers.rb | 1 + lib/secure_headers/headers/clear_site_data.rb | 51 +++++++++ .../headers/clear_site_data_spec.rb | 103 ++++++++++++++++++ 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 lib/secure_headers/headers/clear_site_data.rb create mode 100644 spec/lib/secure_headers/headers/clear_site_data_spec.rb diff --git a/README.md b/README.md index 912a5c22..ad11584c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The gem will automatically apply several headers that are related to security. - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) +- Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://www.w3.org/TR/clear-site-data/). It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so). @@ -111,7 +112,7 @@ end ## Default values -All headers except for PublicKeyPins have a default value. The default set of headers is: +All headers except for PublicKeyPins and ClearSiteData have a default value. The default set of headers is: ``` Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 051d5296..c51c1d01 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -10,6 +10,7 @@ require "secure_headers/headers/x_download_options" require "secure_headers/headers/x_permitted_cross_domain_policies" require "secure_headers/headers/referrer_policy" +require "secure_headers/headers/clear_site_data" require "secure_headers/middleware" require "secure_headers/railtie" require "secure_headers/view_helper" diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb new file mode 100644 index 00000000..4cd8b208 --- /dev/null +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -0,0 +1,51 @@ +module SecureHeaders + class ClearSiteDataConfigError < StandardError; end + class ClearSiteData + HEADER_NAME = "Clear-Site-Data".freeze + TYPES = "types".freeze + + # Valid `types` + CACHE = "cache".freeze + COOKIES = "cookies".freeze + STORAGE = "storage".freeze + EXECTION_CONTEXTS = "executionContexts".freeze + ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_CONTEXTS] + + CONFIG_KEY = :clear_site_data + + class << self + # Public: make an Clear-Site-Data header name, value pair + # + # Returns nil if not configured, returns header name and value if configured. + def make_header(config=nil) + case config + when nil, OPT_OUT, [] + # noop + when Array + [HEADER_NAME, JSON.dump(TYPES => config)] + when true + [HEADER_NAME, JSON.dump(TYPES => ALL_TYPES)] + end + end + + def validate_config!(config) + case config + when nil, OPT_OUT, true + # valid + when Array + unless config.all? { |t| t.is_a?(String) } + raise ClearSiteDataConfigError.new("types must be Strings") + end + + begin + JSON.dump(config) + rescue JSON::GeneratorError + raise ClearSiteDataConfigError.new("types must serializable by JSON") + end + else + raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`") + end + end + end + end +end diff --git a/spec/lib/secure_headers/headers/clear_site_data_spec.rb b/spec/lib/secure_headers/headers/clear_site_data_spec.rb new file mode 100644 index 00000000..e065583c --- /dev/null +++ b/spec/lib/secure_headers/headers/clear_site_data_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +module SecureHeaders + describe ClearSiteData do + describe "make_header" do + it "returns nil with nil config" do + expect(described_class.make_header).to be_nil + end + + it "returns nil with empty config" do + expect(described_class.make_header([])).to be_nil + end + + it "returns nil with opt-out config" do + expect(described_class.make_header(OPT_OUT)).to be_nil + end + + it "returns all types with `true` config" do + name, value = described_class.make_header(true) + + expect(name).to eq(ClearSiteData::HEADER_NAME) + expect(value).to eq(normalize_json(<<-HERE)) + { + "types": [ + "cache", + "cookies", + "storage", + "executionContexts" + ] + } + HERE + end + + it "returns specified types" do + name, value = described_class.make_header(["foo", "bar"]) + + expect(name).to eq(ClearSiteData::HEADER_NAME) + expect(value).to eq(normalize_json(<<-HERE)) + { + "types": [ + "foo", + "bar" + ] + } + HERE + end + end + + describe "validate_config!" do + it "succeeds for `true` config" do + expect do + described_class.validate_config!(true) + end.not_to raise_error + end + + it "succeeds for `nil` config" do + expect do + described_class.validate_config!(nil) + end.not_to raise_error + end + + it "succeeds for opt-out config" do + expect do + described_class.validate_config!(OPT_OUT) + end.not_to raise_error + end + + it "succeeds for empty config" do + expect do + described_class.validate_config!([]) + end.not_to raise_error + end + + it "succeeds for Array of Strings config" do + expect do + described_class.validate_config!(["foo"]) + end.not_to raise_error + end + + it "fails for Array of non-String config" do + expect do + described_class.validate_config!([1]) + end.to raise_error(ClearSiteDataConfigError) + end + + it "fails for non-serializable config" do + expect do + described_class.validate_config!(["hi \255"]) + end.to raise_error(ClearSiteDataConfigError) + end + + it "fails for other types of config" do + expect do + described_class.validate_config!(:cookies) + end.to raise_error(ClearSiteDataConfigError) + end + end + + def normalize_json(json) + JSON.dump(JSON.parse(json)) + end + end +end From 666d4d6c04bf5e8f6395355bda370f7f736078e6 Mon Sep 17 00:00:00 2001 From: Benjamin P Toews Date: Wed, 4 Jan 2017 11:07:52 -0700 Subject: [PATCH 327/636] different exception for invalid utf8 in 1.9.3 --- lib/secure_headers/headers/clear_site_data.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index 4cd8b208..c078caab 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -39,7 +39,7 @@ def validate_config!(config) begin JSON.dump(config) - rescue JSON::GeneratorError + rescue JSON::GeneratorError, Encoding::UndefinedConversionError raise ClearSiteDataConfigError.new("types must serializable by JSON") end else From 4d128a87e05683d6313ce29c2611f8a0c37938a2 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 5 Jan 2017 09:26:08 -1000 Subject: [PATCH 328/636] add functionality to validate/set the header via the config --- README.md | 6 ++++++ lib/secure_headers.rb | 1 + lib/secure_headers/configuration.rb | 4 +++- spec/lib/secure_headers_spec.rb | 8 ++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ad11584c..51091015 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,12 @@ SecureHeaders::Configuration.default do |config| config.x_download_options = "noopen" config.x_permitted_cross_domain_policies = "none" config.referrer_policy = "origin-when-cross-origin" + config.clear_site_data = [ + "cache", + "cookies", + "storage", + "executionContexts" + ] config.csp = { # "meta" values. these will shaped the header, but the values are not included in the header. report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index c51c1d01..84502f0a 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -51,6 +51,7 @@ def opt_out? CSP = ContentSecurityPolicy ALL_HEADER_CLASSES = [ + ClearSiteData, ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig, StrictTransportSecurity, diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index b0a53df2..13447d8e 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -116,7 +116,7 @@ def deep_copy_if_hash(value) attr_writer :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, - :referrer_policy + :referrer_policy, :clear_site_data attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host @@ -150,6 +150,7 @@ def dup copy.x_xss_protection = @x_xss_protection copy.x_download_options = @x_download_options copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies + copy.clear_site_data = @clear_site_data copy.referrer_policy = @referrer_policy copy.hpkp = @hpkp copy.hpkp_report_host = @hpkp_report_host @@ -181,6 +182,7 @@ def validate_config! XXssProtection.validate_config!(@x_xss_protection) XDownloadOptions.validate_config!(@x_download_options) XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies) + ClearSiteData.validate_config!(@clear_site_data) PublicKeyPins.validate_config!(@hpkp) Cookie.validate_config!(@cookies) end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 5715d838..c6a04a2f 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -530,6 +530,14 @@ module SecureHeaders end.to raise_error(XContentTypeOptionsConfigError) end + it "validates your clear site data config upon configuration" do + expect do + Configuration.default do |config| + config.clear_site_data = 1 + end + end.to raise_error(ClearSiteDataConfigError) + end + it "validates your x_xss config upon configuration" do expect do Configuration.default do |config| From 1b2baf2211e9b16448e72751ef82808400535e96 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 5 Jan 2017 11:21:23 -1000 Subject: [PATCH 329/636] 3.6.0 bump (includes clear-site-data header support) --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f2ce688..9041da6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.6.0 + +Add support for the clear-site-data header + ## 3.5.1 * Fix bug that can occur when useragent library version is older, resulting in a nil version sometimes. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 063a65f5..e7ccaf0c 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.5.1" + gem.version = "3.6.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 40ed074b23cb3e6d90bc90e4c1595ab1aea953cb Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Fri, 6 Jan 2017 17:18:33 -0800 Subject: [PATCH 330/636] Remove `preload` from the default HSTS configuration in the README. See https://hstspreload.org/#opt-in --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 51091015..793ad786 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,8 @@ SecureHeaders::Configuration.default do |config| lax: true # mark all cookies as SameSite=lax } } - config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload" + # Add "; preload" and submit the site to hstspreload.org for best protection. + config.hsts = "max-age=#{20.years.to_i}; includeSubdomains" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" config.x_xss_protection = "1; mode=block" From b4ba03ae77a289210403c991da5d6b830d313ba9 Mon Sep 17 00:00:00 2001 From: BePsvPT Date: Sat, 7 Jan 2017 16:31:48 +0800 Subject: [PATCH 331/636] Add link to php secure-headers --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 793ad786..c01e36f1 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ end * Elixir [secure_headers](https://github.com/anotherhale/secure_headers) * Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security) * Ember.js [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/) +* PHP [secure-headers](https://github.com/BePsvPT/secure-headers) ## License From 43d4101e2a7826e4ec6914e8e9d9a81d66eb01b2 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 15:17:19 -0200 Subject: [PATCH 332/636] Include Ruby 2.3 in the test matrix Ruby 2.4 needs to wait on a new Rails 4.2.x release which includes some fixes for that Ruby version, so going only with Ruby 2.3.x for now. --- .ruby-version | 2 +- .travis.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 21bb5e15..0bee604d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.2.5 +2.3.3 diff --git a/.travis.yml b/.travis.yml index 91c09075..b7978c9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: ruby rvm: - ruby-head + - 2.3 - 2.2 - 2.1 - 2.0.0 From 51f7b749c0fbc705690c9dc70a3d4ef97bbc8592 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 18:27:39 -0200 Subject: [PATCH 333/636] Cache the config attributes to prevent creating multiple objects at runtime --- lib/secure_headers/headers/content_security_policy_config.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 52d5c673..03f9833d 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -88,8 +88,9 @@ class ContentSecurityPolicyConfig CONFIG_KEY = :csp HEADER_NAME = "Content-Security-Policy".freeze + ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES def self.attrs - PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES + ATTRS end include DynamicConfig From bdb959dced2756b131d492654613aa3aefde1003 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 18:34:20 -0200 Subject: [PATCH 334/636] Speed up `to_h` on dynamic config by performing a single loop through attrs Avoid initializing the hash with a `nil` value just to reject it later. --- lib/secure_headers/headers/content_security_policy_config.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 03f9833d..9ab36401 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -52,8 +52,9 @@ def append(new_hash) def to_h self.class.attrs.each_with_object({}) do |key, hash| - hash[key] = self.send(key) - end.reject { |_, v| v.nil? } + value = self.send(key) + hash[key] = value unless value.nil? + end end def dup From f7115c33550622b846e1953ffd04c1a46f2aafcd Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 18:41:05 -0200 Subject: [PATCH 335/636] Cache instance variable name to prevent creating the same string 3 times --- .../headers/content_security_policy_config.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 9ab36401..679747f1 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -7,9 +7,10 @@ def self.included(base) base.send(:define_method, "#{attr}=") do |value| if self.class.attrs.include?(attr) value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list - prev_value = self.instance_variable_get("@#{attr}") - self.instance_variable_set("@#{attr}", value) - if prev_value != self.instance_variable_get("@#{attr}") + attr_variable = "@#{attr}" + prev_value = self.instance_variable_get(attr_variable) + self.instance_variable_set(attr_variable, value) + if prev_value != self.instance_variable_get(attr_variable) @modified = true end else From 4e0f916615b5dd3f92a97d08bb5530bd9e8c081d Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 18:44:10 -0200 Subject: [PATCH 336/636] Loop a single time over the hash to set configs Reject `nil` values within the loop, instead of performing an initial loop just to reject them. Loop using `each_pair` as we already get both key and value, without needing to perform another hash lookup to grab the value. --- .../headers/content_security_policy_config.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 679747f1..510f325a 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -75,11 +75,13 @@ def ==(o) private def from_hash(hash) - hash.keys.reject { |k| hash[k].nil? }.map do |k| + hash.each_pair do |k, v| + next if v.nil? + if self.class.attrs.include?(k) - self.send("#{k}=", hash[k]) + self.send("#{k}=", v) else - raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}" + raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}" end end end From d3c9662c9b842e99edab42bee0137562d6d73f9e Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 18:53:26 -0200 Subject: [PATCH 337/636] Extract logic to write attribute to prevent double checking for the attribute being valid When initializing the config from hash we need to check if it's part of the known list of attributes, and the same happens when writing the attribute (I'm still wondering why we need to do that check, since the methods are initialized from the list of attributes, but decided not to touch it for now). Extracting this logic allows us to bypass the duplicated inclusion check when initializing from hash. It also bypasses `send` which helps speed things up. --- .../headers/content_security_policy_config.rb | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 510f325a..6c4a1c7e 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -6,13 +6,7 @@ def self.included(base) base.attrs.each do |attr| base.send(:define_method, "#{attr}=") do |value| if self.class.attrs.include?(attr) - value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list - attr_variable = "@#{attr}" - prev_value = self.instance_variable_get(attr_variable) - self.instance_variable_set(attr_variable, value) - if prev_value != self.instance_variable_get(attr_variable) - @modified = true - end + write_attribute(attr, value) else raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}" end @@ -79,12 +73,22 @@ def from_hash(hash) next if v.nil? if self.class.attrs.include?(k) - self.send("#{k}=", v) + write_attribute(k, v) else raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}" end end end + + def write_attribute(attr, value) + value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list + attr_variable = "@#{attr}" + prev_value = self.instance_variable_get(attr_variable) + self.instance_variable_set(attr_variable, value) + if prev_value != self.instance_variable_get(attr_variable) + @modified = true + end + end end class ContentSecurityPolicyConfigError < StandardError; end From 45f8530ebe5226035f4904ca89750c44989feead Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 19:05:08 -0200 Subject: [PATCH 338/636] Perform lop using `each_key` instead of creating intermediate array --- lib/secure_headers/headers/policy_management.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 20d45753..eae17634 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -271,7 +271,7 @@ def merge_policy_additions(original, additions) # copy the default-src value to the original config. This modifies the original hash. def populate_fetch_source_with_default!(original, additions) # in case we would be appending to an empty directive, fill it with the default-src value - additions.keys.each do |directive| + additions.each_key do |directive| if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions)) if nonce_added?(original, additions) inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym From 13c354cea6e5e3ef9ede7afd3e6147c91b9bbf3b Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 19:22:40 -0200 Subject: [PATCH 339/636] Freeze all header names Since the library builds a hash with all the headers many many times, it ends up retaining a lot of strings since string hash keys are duped by default. By freezing the keys we ensure the duping doesn't happen, decreasing the number of objects created. --- lib/secure_headers/headers/referrer_policy.rb | 2 +- lib/secure_headers/headers/strict_transport_security.rb | 2 +- lib/secure_headers/headers/x_content_type_options.rb | 2 +- lib/secure_headers/headers/x_download_options.rb | 2 +- lib/secure_headers/headers/x_frame_options.rb | 2 +- lib/secure_headers/headers/x_permitted_cross_domain_policies.rb | 2 +- lib/secure_headers/headers/x_xss_protection.rb | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index 63898b6f..d7366c6c 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -1,7 +1,7 @@ module SecureHeaders class ReferrerPolicyConfigError < StandardError; end class ReferrerPolicy - HEADER_NAME = "Referrer-Policy" + HEADER_NAME = "Referrer-Policy".freeze DEFAULT_VALUE = "origin-when-cross-origin" VALID_POLICIES = %w( no-referrer diff --git a/lib/secure_headers/headers/strict_transport_security.rb b/lib/secure_headers/headers/strict_transport_security.rb index 9ec72748..25269d93 100644 --- a/lib/secure_headers/headers/strict_transport_security.rb +++ b/lib/secure_headers/headers/strict_transport_security.rb @@ -2,7 +2,7 @@ module SecureHeaders class STSConfigError < StandardError; end class StrictTransportSecurity - HEADER_NAME = 'Strict-Transport-Security' + HEADER_NAME = 'Strict-Transport-Security'.freeze HSTS_MAX_AGE = "631138519" DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index 65e583e8..a144cc85 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -2,7 +2,7 @@ module SecureHeaders class XContentTypeOptionsConfigError < StandardError; end class XContentTypeOptions - HEADER_NAME = "X-Content-Type-Options" + HEADER_NAME = "X-Content-Type-Options".freeze DEFAULT_VALUE = "nosniff" CONFIG_KEY = :x_content_type_options diff --git a/lib/secure_headers/headers/x_download_options.rb b/lib/secure_headers/headers/x_download_options.rb index cabf1feb..e3a96cf0 100644 --- a/lib/secure_headers/headers/x_download_options.rb +++ b/lib/secure_headers/headers/x_download_options.rb @@ -1,7 +1,7 @@ module SecureHeaders class XDOConfigError < StandardError; end class XDownloadOptions - HEADER_NAME = "X-Download-Options" + HEADER_NAME = "X-Download-Options".freeze DEFAULT_VALUE = 'noopen' CONFIG_KEY = :x_download_options diff --git a/lib/secure_headers/headers/x_frame_options.rb b/lib/secure_headers/headers/x_frame_options.rb index ab6fb3fc..c8d7e654 100644 --- a/lib/secure_headers/headers/x_frame_options.rb +++ b/lib/secure_headers/headers/x_frame_options.rb @@ -1,7 +1,7 @@ module SecureHeaders class XFOConfigError < StandardError; end class XFrameOptions - HEADER_NAME = "X-Frame-Options" + HEADER_NAME = "X-Frame-Options".freeze CONFIG_KEY = :x_frame_options SAMEORIGIN = "sameorigin" DENY = "deny" diff --git a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb index acac98aa..c153d844 100644 --- a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +++ b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb @@ -1,7 +1,7 @@ module SecureHeaders class XPCDPConfigError < StandardError; end class XPermittedCrossDomainPolicies - HEADER_NAME = "X-Permitted-Cross-Domain-Policies" + HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze DEFAULT_VALUE = 'none' VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) CONFIG_KEY = :x_permitted_cross_domain_policies diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index ec8b261f..a542b8ae 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -1,7 +1,7 @@ module SecureHeaders class XXssProtectionConfigError < StandardError; end class XXssProtection - HEADER_NAME = 'X-XSS-Protection' + HEADER_NAME = 'X-XSS-Protection'.freeze DEFAULT_VALUE = "1; mode=block" VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i CONFIG_KEY = :x_xss_protection From 4bd8b9b0269bab7708ce298fb55b652e292e6178 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 19:33:57 -0200 Subject: [PATCH 340/636] Simplify the modified check on the config to avoid metaprogramming --- lib/secure_headers/headers/content_security_policy_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 6c4a1c7e..b544e46d 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -85,7 +85,7 @@ def write_attribute(attr, value) attr_variable = "@#{attr}" prev_value = self.instance_variable_get(attr_variable) self.instance_variable_set(attr_variable, value) - if prev_value != self.instance_variable_get(attr_variable) + if prev_value != value @modified = true end end From 84e30f7e53bce59d312d5d6e482d6a63921f1ea6 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 19:40:31 -0200 Subject: [PATCH 341/636] Fix type [ci skip] --- lib/secure_headers/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 13447d8e..ea2725ca 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -262,7 +262,7 @@ def cache_hpkp_report_host end end - # Public: Precompute the header names and values for this configuraiton. + # Public: Precompute the header names and values for this configuration. # Ensures that headers generated at configure time, not on demand. # # Returns the cached headers From 9dceb6e140674981be833e6ab172477f9f0f6406 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 19:45:45 -0200 Subject: [PATCH 342/636] Avoid creating extra arrays when building the headers hash We only need to check for two different classes in here, so it's just better/faster to do the individual comparisons than building an extra array per header that is being generated. --- lib/secure_headers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 84502f0a..cf0f91eb 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -177,7 +177,7 @@ def header_hash_for(request) header_classes_for(request).each_with_object({}) do |klass, hash| if header = headers[klass::CONFIG_KEY] - header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass) + header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig csp_header_for_ua(header, user_agent) else header From 35d3c219ce8eb1ded1dd12e09b55030a956a1ecb Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 17 Jan 2017 19:53:38 -0200 Subject: [PATCH 343/636] Remove unneeded flatten --- lib/secure_headers/headers/policy_management.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index eae17634..3e03a42a 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -109,7 +109,7 @@ def self.included(base) DIRECTIVES_2_0 + DIRECTIVES_DRAFT ).freeze - ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort + ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT).uniq.sort # Think of default-src and report-uri as the beginning and end respectively, # everything else is in between. From 85d4879d7359424a1be0f40e8a3059672d9b3558 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 18 Jan 2017 09:32:38 -0200 Subject: [PATCH 344/636] Use full version for Ruby 2.3 on travis, otherwise it fails --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b7978c9e..e868a905 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: ruby rvm: - ruby-head - - 2.3 + - 2.3.3 - 2.2 - 2.1 - 2.0.0 From 7ac8eb98b775f18b854ef3a71deab646f4aa65f4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 18 Jan 2017 07:16:13 -1000 Subject: [PATCH 345/636] bump to 3.6.1 --- CHANGELOG.md | 5 +++++ secure_headers.gemspec | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9041da6f..b2b323df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.6.1 + +Improved memory use via minor improvements clever hacks that are sadly needed. +Thanks @carlosantoniodasilva! + ## 3.6.0 Add support for the clear-site-data header diff --git a/secure_headers.gemspec b/secure_headers.gemspec index e7ccaf0c..4148b8ec 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.6.0" + gem.version = "3.6.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 5638cb03344a8b5f02024735143d2261cff08a94 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 16 Feb 2017 10:49:43 -1000 Subject: [PATCH 346/636] Update LICENSE --- LICENSE | 203 ++------------------------------------------------------ 1 file changed, 4 insertions(+), 199 deletions(-) diff --git a/LICENSE b/LICENSE index 29610d9c..875f997d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,202 +1,7 @@ +Copyright 2013, 2014, 2015, 2016, 2017 Twitter, Inc. - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2011 Julio Capote, Christopher Burnett - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From a140b8d2bd94d54f7164625aac3c8b6176fc0ee5 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 26 Dec 2016 22:52:45 -0700 Subject: [PATCH 347/636] Add Ruby 2.3 and 2.4 to test matrix. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e868a905..ea933a1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: ruby rvm: - ruby-head + - 2.4.0 - 2.3.3 - 2.2 - 2.1 From 86b270b53d286149ad57fda684310568dbe90920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20Mark=C5=ABnas?= Date: Fri, 3 Mar 2017 14:50:42 +0200 Subject: [PATCH 348/636] Added CSP2 support for Safari 10+ --- lib/secure_headers/headers/content_security_policy.rb | 2 ++ lib/secure_headers/headers/policy_management.rb | 2 ++ .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 44ce1c72..23342e14 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -216,6 +216,8 @@ def supported_directives @supported_directives ||= if VARIATIONS[@parsed_ua.browser] if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46) VARIATIONS["FirefoxTransitional"] + elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10) + VARIATIONS["SafariTransitional"] else VARIATIONS[@parsed_ua.browser] end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 3e03a42a..52337d63 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -81,6 +81,7 @@ def self.included(base) EDGE_DIRECTIVES = DIRECTIVES_1_0 SAFARI_DIRECTIVES = DIRECTIVES_1_0 + SAFARI_10_DIRECTIVES = DIRECTIVES_2_0 FIREFOX_UNSUPPORTED_DIRECTIVES = [ BLOCK_ALL_MIXED_CONTENT, @@ -133,6 +134,7 @@ def self.included(base) "Firefox" => FIREFOX_DIRECTIVES, "FirefoxTransitional" => FIREFOX_46_DIRECTIVES, "Safari" => SAFARI_DIRECTIVES, + "SafariTransitional" => SAFARI_10_DIRECTIVES, "Edge" => EDGE_DIRECTIVES, "Other" => CHROME_DIRECTIVES }.freeze diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 8e43601f..b4bd6624 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -155,6 +155,11 @@ module SecureHeaders expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end + it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do + policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10]) + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com") + end + it "falls back to standard Firefox defaults when the useragent version is not present" do ua = USER_AGENTS[:firefox].dup allow(ua).to receive(:version).and_return(nil) From b69b1fae3754d37522cd1f7289076feba5b2e707 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 3 Mar 2017 08:40:47 -1000 Subject: [PATCH 349/636] comment out report_only: true --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c01e36f1..2c6127d1 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ SecureHeaders::Configuration.default do |config| ] config.csp = { # "meta" values. these will shaped the header, but the values are not included in the header. - report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] + # report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives From ae1d20d11d5b5eae43dc7aa0a1c308f6dc980adb Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 3 Mar 2017 08:45:26 -1000 Subject: [PATCH 350/636] update changelog for 3.6.2 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b323df..20355a48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.6.2 + +Now that Safari 10 supports nonces and it appears to work, enable the nonce feature for safari. + ## 3.6.1 Improved memory use via minor improvements clever hacks that are sadly needed. From 090b87d349fbff86a02023ae77695e165d50f36f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 3 Mar 2017 08:50:30 -1000 Subject: [PATCH 351/636] version bump --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 4148b8ec..7a71de51 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.6.1" + gem.version = "3.6.2" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Security related headers all in one gem.' From 5fda0fa7f3733c9a4d24d5061e7916f6dfbe04d4 Mon Sep 17 00:00:00 2001 From: Bob Long Date: Thu, 13 Apr 2017 13:58:05 +0100 Subject: [PATCH 352/636] remove deprecation warning about frame_src --- lib/secure_headers/headers/content_security_policy.rb | 2 -- .../secure_headers/headers/content_security_policy_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 23342e14..8e23e12d 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -59,8 +59,6 @@ def value def normalize_child_frame_src if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.") - elsif @config.frame_src - Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config.frame_src}.") end if supported_directives.include?(:child_src) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index b4bd6624..1570f09f 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -86,8 +86,8 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org") end - it "emits a warning when using frame-src" do - expect(Kernel).to receive(:warn).with(/:frame_src is deprecated, use :child_src instead./) + it "does not emit a warning when using frame-src" do + expect(Kernel).to_not receive(:warn) ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value end From 466664bd70e1641c0d61e3a657553120c1c905f3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 21 Apr 2017 09:17:33 -1000 Subject: [PATCH 353/636] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..5b6d86dd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at neil.matatall@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ From d9bed91704afb688221663a38b10ec86bfd56e83 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 21 Apr 2017 09:18:48 -1000 Subject: [PATCH 354/636] Create CONTRIBUTING.md --- CONTRIBUTING.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..86589c87 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +## Contributing + +[fork]: https://github.com/twitter/secureheaders/fork +[pr]: https://github.com/twitter/secureheaders/compare +[style]: https://github.com/styleguide/ruby +[code-of-conduct]: CODE_OF_CONDUCT.md + +Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. + +Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. + +## Submitting a pull request + +0. [Fork][fork] and clone the repository +0. Configure and install the dependencies: `bundle install` +0. Make sure the tests pass on your machine: `bundle exec rspec spec` +0. Create a new branch: `git checkout -b my-branch-name` +0. Make your change, add tests, and make sure the tests still pass +0. Push to your fork and [submit a pull request][pr] +0. Pat your self on the back and wait for your pull request to be reviewed and merged. + +Here are a few things you can do that will increase the likelihood of your pull request being accepted: + +- Write tests. +- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. +- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). + +## Releasing + +0. Ensure CI is green +0. Pull the latest code +0. Increment the version +0. Run `gem build darrrr.gemspec` +0. Bump the Gemfile and Gemfile.lock versions for an app which relies on this gem +0. Test behavior locally, branch deploy, whatever needs to happen +0. Run `bundle exec rake release` + +## Resources + +- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) +- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) + + From dc972de16fe97db29ae65f6d1543cd58c0629e03 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 25 Apr 2017 08:59:14 -1000 Subject: [PATCH 355/636] bump to 3.6.3 --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20355a48..9803feea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.6.3 + +Remove deprecation warning when setting `frame-src`. It is no longer deprecated. + ## 3.6.2 Now that Safari 10 supports nonces and it appears to work, enable the nonce feature for safari. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 7a71de51..b906345c 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,10 +1,10 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.6.2" + gem.version = "3.6.3" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] - gem.description = 'Security related headers all in one gem.' + gem.description = 'Manages application of security headers with many safe defaults.' gem.summary = 'Add easily configured security headers to responses including content-security-policy, x-frame-options, strict-transport-security, etc.' From 587c40e71e56ba6c5b7231b1c47f9587dcf05a28 Mon Sep 17 00:00:00 2001 From: Bob Long Date: Wed, 26 Apr 2017 14:18:18 +0100 Subject: [PATCH 356/636] prevent loss of frame_src/child_src when using append_content_security_policy_directives --- .../headers/policy_management.rb | 10 +++++-- spec/lib/secure_headers_spec.rb | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 52337d63..2ca06c90 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -278,15 +278,21 @@ def populate_fetch_source_with_default!(original, additions) if nonce_added?(original, additions) inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive) - original[inferred_directive] = original[:default_src] + original[inferred_directive] = default_for(directive, original) end else - original[directive] = original[:default_src] + original[directive] = default_for(directive, original) end end end end + def default_for(directive, original) + return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC] + return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC] + original[DEFAULT_SRC] + end + def nonce_added?(original, additions) [:script_nonce, :style_nonce].each do |nonce| if additions[nonce] && !original[nonce] diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index c6a04a2f..be2ba778 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -173,6 +173,33 @@ module SecureHeaders expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end + it "appends child-src to frame-src" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + frame_src: %w(frame_src.com) + } + end + + SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com)) + hash = SecureHeaders.header_hash_for(chrome_request) + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; child-src frame_src.com child_src.com") + end + + it "appends frame-src to child-src" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + child_src: %w(child_src.com) + } + end + + safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari6])) + SecureHeaders.append_content_security_policy_directives(safari_request, frame_src: %w(frame_src.com)) + hash = SecureHeaders.header_hash_for(safari_request) + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src child_src.com frame_src.com") + end + it "supports named appends" do Configuration.default do |config| config.csp = { From 0966fb9088c93453bb97fa77f5f2531cd9fa0055 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 3 May 2017 09:00:04 -1000 Subject: [PATCH 357/636] bump 3.6.4 --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9803feea..ace834a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.6.4 + +Fix case where mixing frame-src/child-src dynamically would behave in unexpected ways: https://github.com/twitter/secureheaders/pull/325 + ## 3.6.3 Remove deprecation warning when setting `frame-src`. It is no longer deprecated. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index b906345c..e910a820 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.6.3" + gem.version = "3.6.4" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Manages application of security headers with many safe defaults.' From 5d9b015d3dd11f8180a6b64c7f13d8f5764c8b46 Mon Sep 17 00:00:00 2001 From: Scott Goci Date: Wed, 10 May 2017 17:04:19 -0400 Subject: [PATCH 358/636] Updated a few places where wrong wordage was being used for where and how to configure hashes --- CHANGELOG.md | 2 +- docs/hashes.md | 2 +- lib/secure_headers/view_helper.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace834a5..1db575e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -196,7 +196,7 @@ You can add hash sources directly to your policy : rake secure_headers:generate_hashes ``` - This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. + This will generate a file (`config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. ```yaml --- diff --git a/docs/hashes.md b/docs/hashes.md index b917a204..752d42a5 100644 --- a/docs/hashes.md +++ b/docs/hashes.md @@ -21,7 +21,7 @@ You can add hash sources directly to your policy : rake secure_headers:generate_hashes ``` - This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. + This will generate a file (`config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. ```yaml --- diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index d5ceb329..b4059247 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -95,9 +95,9 @@ def unexpected_hash_error_message(file_path, content, hash_value) <<-EOF \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} *** #{content} -*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:*** +*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:*** #{file_path}: -- #{hash_value}\n\n +- \"#{hash_value}\"\n\n NOTE: dynamic javascript is not supported using script hash integration on purpose. It defeats the point of using it in the first place. EOF From bf32b1d66ae5f5b2c6fd38e4c9a995e619c17c7f Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 25 May 2017 15:05:52 -0400 Subject: [PATCH 359/636] enable warnings when running tests --- .rspec | 1 + 1 file changed, 1 insertion(+) diff --git a/.rspec b/.rspec index fb4b5a2a..32fe547d 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,2 @@ --order rand +--warnings From 0d1c2a4d00f87f5a0e20774f96584c7eb5ceb59c Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 25 May 2017 15:07:56 -0400 Subject: [PATCH 360/636] fix warnings from running in verbose mode --- lib/secure_headers.rb | 2 +- lib/secure_headers/configuration.rb | 23 ++++++++++++++--- .../headers/content_security_policy_config.rb | 25 +++++++++++++++++++ .../headers/policy_management.rb | 6 ++--- lib/secure_headers/headers/public_key_pins.rb | 2 +- spec/lib/secure_headers/view_helpers_spec.rb | 1 - spec/lib/secure_headers_spec.rb | 8 +++--- 7 files changed, 54 insertions(+), 13 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index cf0f91eb..f4e52962 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -163,7 +163,7 @@ def opt_out_of_all_protection(request) # returned is meant to be merged into the header value from `@app.call(env)` # in Rack middleware. def header_hash_for(request) - config = config_for(request, prevent_dup = true) + config = config_for(request, _prevent_dup = true) headers = config.cached_headers user_agent = UserAgent.parse(request.user_agent) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index ea2725ca..99777347 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -31,7 +31,7 @@ def override(name, base = DEFAULT_CONFIG, &block) raise NotYetConfiguredError, "#{base} policy not yet supplied" end override = @configurations[base].dup - override.instance_eval &block if block_given? + override.instance_eval(&block) if block_given? add_configuration(name, override) end @@ -120,19 +120,36 @@ def deep_copy_if_hash(value) attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host + @script_hashes = nil + @style_hashes = nil + HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml" - if File.exists?(HASH_CONFIG_FILE) + if File.exist?(HASH_CONFIG_FILE) config = YAML.safe_load(File.open(HASH_CONFIG_FILE)) @script_hashes = config["scripts"] @style_hashes = config["styles"] end def initialize(&block) + @cookies = nil + @clear_site_data = nil + @csp = nil + @csp_report_only = nil + @hpkp_report_host = nil + @hpkp = nil + @hsts = nil + @x_content_type_options = nil + @x_download_options = nil + @x_frame_options = nil + @x_permitted_cross_domain_policies = nil + @x_xss_protection = nil + self.hpkp = OPT_OUT self.referrer_policy = OPT_OUT self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT) self.csp_report_only = OPT_OUT - instance_eval &block if block_given? + + instance_eval(&block) if block_given? end # Public: copy everything but the cached headers diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index b544e46d..3b8a1473 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -15,6 +15,31 @@ def self.included(base) end def initialize(hash) + @base_uri = nil + @block_all_mixed_content = nil + @child_src = nil + @connect_src = nil + @default_src = nil + @font_src = nil + @form_action = nil + @frame_ancestors = nil + @frame_src = nil + @img_src = nil + @manifest_src = nil + @media_src = nil + @object_src = nil + @plugin_types = nil + @preserve_schemes = nil + @reflected_xss = nil + @report_only = nil + @report_uri = nil + @sandbox = nil + @script_nonce = nil + @script_src = nil + @style_nonce = nil + @style_src = nil + @upgrade_insecure_requests = nil + from_hash(hash) @modified = false end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 2ca06c90..91b62353 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -348,9 +348,9 @@ def ensure_array_of_strings!(directive, source_expression) end def ensure_valid_sources!(directive, source_expression) - source_expression.each do |source_expression| - if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression) - raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{source_expression}). This value must be single quoted.") + source_expression.each do |expression| + if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression) + raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.") end end end diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb index 1016a6e2..943f41a6 100644 --- a/lib/secure_headers/headers/public_key_pins.rb +++ b/lib/secure_headers/headers/public_key_pins.rb @@ -49,7 +49,7 @@ def name end def value - header_value = [ + [ max_age_directive, pin_directives, report_uri_directive, diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 5e25470b..d8ab1629 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -37,7 +37,6 @@ def self.template background-color: black; } -<%= @name %> TEMPLATE end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index be2ba778..f5e330ab 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -105,7 +105,7 @@ module SecureHeaders end it "produces a UA-specific CSP when overriding (and busting the cache)" do - config = Configuration.default do |config| + Configuration.default do |config| config.csp = { default_src: %w('self'), child_src: %w('self') @@ -231,7 +231,7 @@ module SecureHeaders SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header hash = SecureHeaders.header_hash_for(chrome_request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/ + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/) end it "appends a hash to a missing script-src value" do @@ -243,7 +243,7 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123')) hash = SecureHeaders.header_hash_for(chrome_request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/ + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/) end it "overrides individual directives" do @@ -279,7 +279,7 @@ module SecureHeaders end safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5])) - nonce = SecureHeaders.content_security_policy_script_nonce(safari_request) + SecureHeaders.content_security_policy_script_nonce(safari_request) hash = SecureHeaders.header_hash_for(safari_request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'") end From 4452dd86b6b4f939dc0d403b5753ed5b14bdb69c Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 25 May 2017 16:09:39 -0400 Subject: [PATCH 361/636] update contribution guidelines to mention warnings --- CONTRIBUTING.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86589c87..a4477109 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Please note that this project is released with a [Contributor Code of Conduct][c 0. Configure and install the dependencies: `bundle install` 0. Make sure the tests pass on your machine: `bundle exec rspec spec` 0. Create a new branch: `git checkout -b my-branch-name` -0. Make your change, add tests, and make sure the tests still pass +0. Make your change, add tests, and make sure the tests still pass and that no warnings are raised 0. Push to your fork and [submit a pull request][pr] 0. Pat your self on the back and wait for your pull request to be reviewed and merged. @@ -25,7 +25,7 @@ Here are a few things you can do that will increase the likelihood of your pull - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). -## Releasing +## Releasing 0. Ensure CI is green 0. Pull the latest code @@ -39,5 +39,3 @@ Here are a few things you can do that will increase the likelihood of your pull - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) - - From 29d26965dfb04c4269dc46a997496ca58f3898b8 Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 25 May 2017 17:02:58 -0400 Subject: [PATCH 362/636] suppress warning in #header_hash_for --- lib/secure_headers.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index f4e52962..86971523 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -163,7 +163,8 @@ def opt_out_of_all_protection(request) # returned is meant to be merged into the header value from `@app.call(env)` # in Rack middleware. def header_hash_for(request) - config = config_for(request, _prevent_dup = true) + prevent_dup = true + config = config_for(request, prevent_dup) headers = config.cached_headers user_agent = UserAgent.parse(request.user_agent) From 77214a376d5ca1624d1fc7f364826cf6c3e67e58 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Jun 2017 17:04:02 -1000 Subject: [PATCH 363/636] Drop support for unsupported rubies --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ea933a1f..9951b7f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,7 @@ rvm: - 2.4.0 - 2.3.3 - 2.2 - - 2.1 - - 2.0.0 - - 1.9.3 - - jruby-19mode + - jruby-head matrix: From 81821b22daf76541573ce6952c675588da1dc238 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 1 Jun 2017 17:04:20 -1000 Subject: [PATCH 364/636] Update .travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9951b7f2..c7c3854f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ rvm: - 2.4.0 - 2.3.3 - 2.2 - - jruby-head matrix: From 94763f384abdaf6725bb8fea1b5c075c619fafb1 Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Tue, 30 May 2017 16:31:33 -0400 Subject: [PATCH 365/636] add rubocop dependency --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 77b09935..a4f9f678 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ group :test do gem "json", "~> 1" gem "rack", "~> 1" gem "rspec" + gem "rubocop" gem "coveralls" gem "term-ansicolor", "< 1.4" end From 1385e83049272d66b4344a3c7737d1b1801e67b0 Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Tue, 30 May 2017 16:46:16 -0400 Subject: [PATCH 366/636] add rubocop rake task --- Rakefile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index bec88c68..910d7957 100644 --- a/Rakefile +++ b/Rakefile @@ -5,13 +5,12 @@ require 'net/http' require 'net/https' desc "Run RSpec" +# RSpec::Core::RakeTask.new(:spec) RSpec::Core::RakeTask.new do |t| t.verbose = false t.rspec_opts = "--format progress" end -task default: :spec - begin require 'rdoc/task' rescue LoadError @@ -20,9 +19,18 @@ rescue LoadError RDoc::Task = Rake::RDocTask end +begin + require 'rubocop/rake_task' + RuboCop::RakeTask.new +rescue LoadError + task(:rubocop) { $stderr.puts "RuboCop is disabled" } +end + RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = 'SecureHeaders' rdoc.options << '--line-numbers' rdoc.rdoc_files.include('lib/**/*.rb') end + +task default: [:spec, :rubocop] From fcdabb8043f797ce1591cca4dd1dc5ef84701a17 Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Tue, 30 May 2017 16:49:55 -0400 Subject: [PATCH 367/636] add custom rubocop rules --- .rubocop.yml | 26 +++ .rubocop_todo.yml | 567 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 593 insertions(+) create mode 100644 .rubocop.yml create mode 100644 .rubocop_todo.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..d7eabfe5 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,26 @@ +AllCops: + Exclude: + - './Gemfile' + - './Rakefile' + - './Guardfile' + - './secure_headers.gemspec' + - './spec/spec_helper.rb' + - './vendor/bundle/**/*' + DisplayCopNames: true + +Metrics/BlockLength: + Exclude: + - 'spec/**/*_spec.rb' + +Metrics/LineLength: + Exclude: + - 'spec/**/*_spec.rb' + +Metrics/ModuleLength: + Exclude: + - 'spec/**/*_spec.rb' + +Style/FrozenStringLiteralComment: + Enabled: false + +inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..1f84d73b --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,567 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2017-05-30 17:06:48 -0400 using RuboCop version 0.49.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# SupportedStyles: with_first_parameter, with_fixed_indentation +Layout/AlignParameters: + Exclude: + - 'lib/secure_headers/configuration.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, IndentOneStep, IndentationWidth. +# SupportedStyles: case, end +Layout/CaseIndentation: + Exclude: + - 'lib/secure_headers/headers/content_security_policy.rb' + +# Offense count: 9 +# Cop supports --auto-correct. +Layout/ElseAlignment: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/configuration.rb' + - 'lib/secure_headers/headers/content_security_policy.rb' + - 'lib/secure_headers/view_helper.rb' + - 'spec/lib/secure_headers/configuration_spec.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Layout/EmptyLines: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/headers/policy_management.rb' + - 'lib/secure_headers/headers/public_key_pins.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Layout/EmptyLinesAroundAccessModifier: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/headers/content_security_policy_config.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: empty_lines, no_empty_lines +Layout/EmptyLinesAroundBlockBody: + Exclude: + - 'spec/lib/secure_headers_spec.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines +Layout/EmptyLinesAroundClassBody: + Exclude: + - 'lib/secure_headers/headers/cookie.rb' + - 'lib/secure_headers/utils/cookies_config.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: SupportedStyles, IndentationWidth. +# SupportedStyles: special_inside_parentheses, consistent, align_braces +Layout/IndentHash: + EnforcedStyle: consistent + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent +Layout/IndentHeredoc: + Exclude: + - 'lib/secure_headers/view_helper.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: normal, rails +Layout/IndentationConsistency: + Exclude: + - 'spec/lib/secure_headers/headers/policy_management_spec.rb' + +# Offense count: 11 +# Cop supports --auto-correct. +# Configuration parameters: Width, IgnoredPatterns. +Layout/IndentationWidth: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/configuration.rb' + - 'lib/secure_headers/headers/content_security_policy.rb' + - 'lib/secure_headers/view_helper.rb' + - 'spec/lib/secure_headers/configuration_spec.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# SupportedStyles: aligned, indented +Layout/MultilineOperationIndentation: + Exclude: + - 'lib/secure_headers.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Layout/SpaceAfterComma: + Exclude: + - 'lib/secure_headers/headers/cookie.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: space, no_space +Layout/SpaceAroundEqualsInParameterDefault: + Exclude: + - 'lib/secure_headers/headers/clear_site_data.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Layout/SpaceAroundKeyword: + Exclude: + - 'lib/secure_headers/headers/cookie.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters. +# SupportedStyles: space, no_space +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideBlockBraces: + Exclude: + - 'spec/lib/secure_headers/view_helpers_spec.rb' + +# Offense count: 49 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces. +# SupportedStyles: space, no_space, compact +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideHashLiteralBraces: + Exclude: + - 'spec/lib/secure_headers/configuration_spec.rb' + - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' + - 'spec/lib/secure_headers/headers/cookie_spec.rb' + - 'spec/lib/secure_headers/middleware_spec.rb' + - 'spec/lib/secure_headers_spec.rb' + +# Offense count: 3 +# Configuration parameters: AllowSafeAssignment. +Lint/AssignmentInCondition: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/middleware.rb' + +# Offense count: 2 +Lint/EmptyWhen: + Exclude: + - 'lib/secure_headers/headers/clear_site_data.rb' + +# Offense count: 11 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleAlignWith, SupportedStylesAlignWith, AutoCorrect. +# SupportedStylesAlignWith: keyword, variable, start_of_line +Lint/EndAlignment: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/configuration.rb' + - 'lib/secure_headers/headers/content_security_policy.rb' + - 'lib/secure_headers/view_helper.rb' + - 'spec/lib/secure_headers/configuration_spec.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + +# Offense count: 3 +Lint/ParenthesesAsGroupedExpression: + Exclude: + - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' + - 'spec/lib/secure_headers/headers/policy_management_spec.rb' + +# Offense count: 74 +# Cop supports --auto-correct. +Lint/PercentStringArray: + Exclude: + - 'lib/secure_headers/headers/content_security_policy_config.rb' + - 'spec/lib/secure_headers/configuration_spec.rb' + - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' + - 'spec/lib/secure_headers/headers/policy_management_spec.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + - 'spec/lib/secure_headers_spec.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'lib/tasks/tasks.rake' + - 'spec/lib/secure_headers_spec.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. +Lint/UnusedMethodArgument: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/configuration.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + +# Offense count: 15 +Metrics/AbcSize: + Max: 28 + +# Offense count: 1 +# Configuration parameters: CountComments, ExcludedMethods. +Metrics/BlockLength: + Max: 59 + +# Offense count: 2 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 219 + +# Offense count: 9 +Metrics/CyclomaticComplexity: + Max: 8 + +# Offense count: 130 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 286 + +# Offense count: 19 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 26 + +# Offense count: 3 +# Configuration parameters: CountComments. +Metrics/ModuleLength: + Max: 188 + +# Offense count: 9 +Metrics/PerceivedComplexity: + Max: 10 + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: MaxKeyValuePairs. +Performance/RedundantMerge: + Exclude: + - 'lib/secure_headers/middleware.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Performance/StringReplacement: + Exclude: + - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: prefer_alias, prefer_alias_method +Style/Alias: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/configuration.rb' + - 'lib/secure_headers/headers/content_security_policy_config.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods. +# SupportedStyles: line_count_based, semantic, braces_for_chaining +# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object +# FunctionalMethods: let, let!, subject, watch +# IgnoredMethods: lambda, proc, it +Style/BlockDelimiters: + Exclude: + - 'spec/lib/secure_headers/view_helpers_spec.rb' + - 'spec/lib/secure_headers_spec.rb' + +# Offense count: 8 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: braces, no_braces, context_dependent +Style/BracesAroundHashParameters: + Exclude: + - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' + - 'spec/lib/secure_headers_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: Keywords. +# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW +Style/CommentAnnotation: + Exclude: + - 'lib/tasks/tasks.rake' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. +# SupportedStyles: assign_to_condition, assign_inside_condition +Style/ConditionalAssignment: + Exclude: + - 'lib/secure_headers/headers/content_security_policy.rb' + +# Offense count: 24 +Style/Documentation: + Enabled: false + +# Offense count: 2 +Style/DoubleNegation: + Exclude: + - 'lib/secure_headers/headers/public_key_pins.rb' + +# Offense count: 23 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/configuration.rb' + - 'lib/secure_headers/headers/content_security_policy_config.rb' + - 'lib/secure_headers/headers/policy_management.rb' + - 'lib/secure_headers/headers/public_key_pins.rb' + - 'lib/secure_headers/headers/referrer_policy.rb' + - 'lib/secure_headers/headers/x_content_type_options.rb' + - 'lib/secure_headers/headers/x_download_options.rb' + - 'lib/secure_headers/headers/x_frame_options.rb' + - 'lib/secure_headers/headers/x_permitted_cross_domain_policies.rb' + - 'lib/secure_headers/middleware.rb' + - 'lib/secure_headers/utils/cookies_config.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. +# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys +Style/HashSyntax: + Exclude: + - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + - 'spec/lib/secure_headers_spec.rb' + +# Offense count: 2 +Style/IfInsideElse: + Exclude: + - 'lib/secure_headers/configuration.rb' + +# Offense count: 12 +# Cop supports --auto-correct. +# Configuration parameters: MaxLineLength. +Style/IfUnlessModifier: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/headers/content_security_policy.rb' + - 'lib/secure_headers/headers/content_security_policy_config.rb' + - 'lib/secure_headers/headers/policy_management.rb' + - 'lib/secure_headers/middleware.rb' + - 'lib/tasks/tasks.rake' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: line_count_dependent, lambda, literal +Style/Lambda: + Exclude: + - 'spec/lib/secure_headers/middleware_spec.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + +# Offense count: 1 +Style/MultilineBlockChain: + Exclude: + - 'lib/secure_headers/headers/policy_management.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/MultilineIfModifier: + Exclude: + - 'lib/secure_headers/view_helper.rb' + +# Offense count: 1 +Style/MultipleComparison: + Exclude: + - 'lib/secure_headers.rb' + +# Offense count: 21 +# Cop supports --auto-correct. +Style/MutableConstant: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/configuration.rb' + - 'lib/secure_headers/headers/clear_site_data.rb' + - 'lib/secure_headers/headers/content_security_policy_config.rb' + - 'lib/secure_headers/headers/policy_management.rb' + - 'lib/secure_headers/headers/referrer_policy.rb' + - 'lib/secure_headers/headers/strict_transport_security.rb' + - 'lib/secure_headers/headers/x_content_type_options.rb' + - 'lib/secure_headers/headers/x_download_options.rb' + - 'lib/secure_headers/headers/x_frame_options.rb' + - 'lib/secure_headers/headers/x_permitted_cross_domain_policies.rb' + - 'lib/secure_headers/headers/x_xss_protection.rb' + - 'lib/secure_headers/middleware.rb' + - 'lib/secure_headers/view_helper.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: both, prefix, postfix +Style/NegatedIf: + Exclude: + - 'lib/secure_headers/utils/cookies_config.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. +# SupportedStyles: skip_modifier_ifs, always +Style/Next: + Exclude: + - 'lib/secure_headers.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: Strict. +Style/NumericLiterals: + MinDigits: 9 + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. +# SupportedStyles: predicate, comparison +Style/NumericPredicate: + Exclude: + - 'spec/**/*' + - 'lib/secure_headers/headers/x_content_type_options.rb' + - 'lib/secure_headers/headers/x_download_options.rb' + +# Offense count: 1 +Style/OpMethod: + Exclude: + - 'lib/secure_headers/headers/content_security_policy_config.rb' + +# Offense count: 134 +# Cop supports --auto-correct. +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'lib/secure_headers/headers/content_security_policy_config.rb' + - 'lib/secure_headers/headers/policy_management.rb' + - 'lib/secure_headers/headers/referrer_policy.rb' + - 'lib/secure_headers/headers/x_permitted_cross_domain_policies.rb' + - 'spec/lib/secure_headers/configuration_spec.rb' + - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' + - 'spec/lib/secure_headers/headers/policy_management_spec.rb' + - 'spec/lib/secure_headers/middleware_spec.rb' + - 'spec/lib/secure_headers/view_helpers_spec.rb' + - 'spec/lib/secure_headers_spec.rb' + +# Offense count: 4 +# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. +# NamePrefix: is_, has_, have_ +# NamePrefixBlacklist: is_, has_, have_ +# NameWhitelist: is_a? +Style/PredicateName: + Exclude: + - 'spec/**/*' + - 'lib/secure_headers/utils/cookies_config.rb' + - 'lib/tasks/tasks.rake' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: short, verbose +Style/PreferredHashMethods: + Exclude: + - 'lib/secure_headers/headers/cookie.rb' + +# Offense count: 38 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: compact, exploded +Style/RaiseArgs: + Exclude: + - 'lib/secure_headers.rb' + - 'lib/secure_headers/headers/clear_site_data.rb' + - 'lib/secure_headers/headers/policy_management.rb' + - 'lib/secure_headers/headers/public_key_pins.rb' + - 'lib/secure_headers/headers/referrer_policy.rb' + - 'lib/secure_headers/headers/strict_transport_security.rb' + - 'lib/secure_headers/headers/x_content_type_options.rb' + - 'lib/secure_headers/headers/x_download_options.rb' + - 'lib/secure_headers/headers/x_frame_options.rb' + - 'lib/secure_headers/headers/x_permitted_cross_domain_policies.rb' + - 'lib/secure_headers/headers/x_xss_protection.rb' + - 'lib/secure_headers/utils/cookies_config.rb' + - 'lib/secure_headers/view_helper.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/RedundantParentheses: + Exclude: + - 'lib/secure_headers/configuration.rb' + +# Offense count: 15 +# Cop supports --auto-correct. +Style/RedundantSelf: + Exclude: + - 'lib/secure_headers/configuration.rb' + - 'lib/secure_headers/headers/content_security_policy_config.rb' + - 'lib/secure_headers/view_helper.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. +# SupportedStyles: slashes, percent_r, mixed +Style/RegexpLiteral: + Exclude: + - 'lib/tasks/tasks.rake' + +# Offense count: 505 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Enabled: false + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiteralsInInterpolation: + Exclude: + - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' + +# Offense count: 9 +# Cop supports --auto-correct. +# Configuration parameters: MinSize, SupportedStyles. +# SupportedStyles: percent, brackets +Style/SymbolArray: + EnforcedStyle: brackets + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInLiteral: + Exclude: + - 'lib/secure_headers/headers/cookie.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. +# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym +Style/TrivialAccessors: + Exclude: + - 'lib/secure_headers/configuration.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: SupportedStyles, WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + EnforcedStyle: percent + MinSize: 3 From 194c14b4eb8179d2d5179361fc62cce0467fa044 Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Wed, 31 May 2017 20:27:59 -0400 Subject: [PATCH 368/636] enable all rubocop cops --- .rubocop.yml | 25 ---------------------- .rubocop_todo.yml | 54 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index d7eabfe5..cc32da4b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,26 +1 @@ -AllCops: - Exclude: - - './Gemfile' - - './Rakefile' - - './Guardfile' - - './secure_headers.gemspec' - - './spec/spec_helper.rb' - - './vendor/bundle/**/*' - DisplayCopNames: true - -Metrics/BlockLength: - Exclude: - - 'spec/**/*_spec.rb' - -Metrics/LineLength: - Exclude: - - 'spec/**/*_spec.rb' - -Metrics/ModuleLength: - Exclude: - - 'spec/**/*_spec.rb' - -Style/FrozenStringLiteralComment: - Enabled: false - inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1f84d73b..5f40b5d1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,11 +1,19 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2017-05-30 17:06:48 -0400 using RuboCop version 0.49.0. +# on 2017-05-31 20:27:20 -0400 using RuboCop version 0.49.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: Include, TreatCommentsAsGroupSeparators. +# Include: **/Gemfile, **/gems.rb +Bundler/OrderedGems: + Exclude: + - 'Gemfile' + # Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. @@ -32,13 +40,20 @@ Layout/ElseAlignment: - 'lib/secure_headers/view_helper.rb' - 'spec/lib/secure_headers/configuration_spec.rb' -# Offense count: 3 +# Offense count: 1 +# Cop supports --auto-correct. +Layout/EmptyLineAfterMagicComment: + Exclude: + - 'secure_headers.gemspec' + +# Offense count: 5 # Cop supports --auto-correct. Layout/EmptyLines: Exclude: - 'lib/secure_headers.rb' - 'lib/secure_headers/headers/policy_management.rb' - 'lib/secure_headers/headers/public_key_pins.rb' + - 'spec/spec_helper.rb' # Offense count: 2 # Cop supports --auto-correct. @@ -193,6 +208,11 @@ Lint/PercentStringArray: - 'spec/lib/secure_headers/view_helpers_spec.rb' - 'spec/lib/secure_headers_spec.rb' +# Offense count: 1 +Lint/ScriptPermission: + Exclude: + - 'Rakefile' + # Offense count: 4 # Cop supports --auto-correct. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. @@ -210,14 +230,14 @@ Lint/UnusedMethodArgument: - 'lib/secure_headers/configuration.rb' - 'spec/lib/secure_headers/view_helpers_spec.rb' -# Offense count: 15 +# Offense count: 16 Metrics/AbcSize: - Max: 28 + Max: 32 -# Offense count: 1 +# Offense count: 30 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: - Max: 59 + Max: 518 # Offense count: 2 # Configuration parameters: CountComments. @@ -228,21 +248,21 @@ Metrics/ClassLength: Metrics/CyclomaticComplexity: Max: 8 -# Offense count: 130 +# Offense count: 367 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Metrics/LineLength: - Max: 286 + Max: 504 # Offense count: 19 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 26 -# Offense count: 3 +# Offense count: 7 # Configuration parameters: CountComments. Metrics/ModuleLength: - Max: 188 + Max: 520 # Offense count: 9 Metrics/PerceivedComplexity: @@ -317,6 +337,13 @@ Style/DoubleNegation: Exclude: - 'lib/secure_headers/headers/public_key_pins.rb' +# Offense count: 42 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: when_needed, always, never +Style/FrozenStringLiteralComment: + Enabled: false + # Offense count: 23 # Configuration parameters: MinBodyLength. Style/GuardClause: @@ -387,7 +414,7 @@ Style/MultipleComparison: Exclude: - 'lib/secure_headers.rb' -# Offense count: 21 +# Offense count: 22 # Cop supports --auto-correct. Style/MutableConstant: Exclude: @@ -405,6 +432,7 @@ Style/MutableConstant: - 'lib/secure_headers/headers/x_xss_protection.rb' - 'lib/secure_headers/middleware.rb' - 'lib/secure_headers/view_helper.rb' + - 'spec/spec_helper.rb' # Offense count: 1 # Cop supports --auto-correct. @@ -520,7 +548,7 @@ Style/RegexpLiteral: Exclude: - 'lib/tasks/tasks.rake' -# Offense count: 505 +# Offense count: 544 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes @@ -535,7 +563,7 @@ Style/StringLiteralsInInterpolation: Exclude: - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' -# Offense count: 9 +# Offense count: 11 # Cop supports --auto-correct. # Configuration parameters: MinSize, SupportedStyles. # SupportedStyles: percent, brackets From a7e2a75a5464d0042c1b6cfece98af55cd344d1e Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Wed, 31 May 2017 20:28:17 -0400 Subject: [PATCH 369/636] split out rspec and rubocop in CI --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index c7c3854f..fc379dad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,12 @@ rvm: - 2.2 - jruby-head +env: + - SUITE=rspec spec + - SUITE=rubocop + +script: bundle exec $SUITE + matrix: allow_failures: - rvm: jruby-head From 194c6e3da3110df80aedd0070ce4d1dbf86dec26 Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 1 Jun 2017 21:55:58 -0400 Subject: [PATCH 370/636] add rubocop-github --- .rubocop.yml | 4 +- .rubocop_todo.yml | 595 ---------------------------------------------- Gemfile | 3 +- 3 files changed, 5 insertions(+), 597 deletions(-) delete mode 100644 .rubocop_todo.yml diff --git a/.rubocop.yml b/.rubocop.yml index cc32da4b..c299de23 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1 +1,3 @@ -inherit_from: .rubocop_todo.yml +inherit_gem: + rubocop-github: + - config/default.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index 5f40b5d1..00000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,595 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config` -# on 2017-05-31 20:27:20 -0400 using RuboCop version 0.49.0. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: Include, TreatCommentsAsGroupSeparators. -# Include: **/Gemfile, **/gems.rb -Bundler/OrderedGems: - Exclude: - - 'Gemfile' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. -# SupportedStyles: with_first_parameter, with_fixed_indentation -Layout/AlignParameters: - Exclude: - - 'lib/secure_headers/configuration.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentOneStep, IndentationWidth. -# SupportedStyles: case, end -Layout/CaseIndentation: - Exclude: - - 'lib/secure_headers/headers/content_security_policy.rb' - -# Offense count: 9 -# Cop supports --auto-correct. -Layout/ElseAlignment: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/configuration.rb' - - 'lib/secure_headers/headers/content_security_policy.rb' - - 'lib/secure_headers/view_helper.rb' - - 'spec/lib/secure_headers/configuration_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Layout/EmptyLineAfterMagicComment: - Exclude: - - 'secure_headers.gemspec' - -# Offense count: 5 -# Cop supports --auto-correct. -Layout/EmptyLines: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/headers/policy_management.rb' - - 'lib/secure_headers/headers/public_key_pins.rb' - - 'spec/spec_helper.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/EmptyLinesAroundAccessModifier: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/headers/content_security_policy_config.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: empty_lines, no_empty_lines -Layout/EmptyLinesAroundBlockBody: - Exclude: - - 'spec/lib/secure_headers_spec.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines -Layout/EmptyLinesAroundClassBody: - Exclude: - - 'lib/secure_headers/headers/cookie.rb' - - 'lib/secure_headers/utils/cookies_config.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: SupportedStyles, IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_braces -Layout/IndentHash: - EnforcedStyle: consistent - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent -Layout/IndentHeredoc: - Exclude: - - 'lib/secure_headers/view_helper.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: normal, rails -Layout/IndentationConsistency: - Exclude: - - 'spec/lib/secure_headers/headers/policy_management_spec.rb' - -# Offense count: 11 -# Cop supports --auto-correct. -# Configuration parameters: Width, IgnoredPatterns. -Layout/IndentationWidth: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/configuration.rb' - - 'lib/secure_headers/headers/content_security_policy.rb' - - 'lib/secure_headers/view_helper.rb' - - 'spec/lib/secure_headers/configuration_spec.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/MultilineOperationIndentation: - Exclude: - - 'lib/secure_headers.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/SpaceAfterComma: - Exclude: - - 'lib/secure_headers/headers/cookie.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: space, no_space -Layout/SpaceAroundEqualsInParameterDefault: - Exclude: - - 'lib/secure_headers/headers/clear_site_data.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/SpaceAroundKeyword: - Exclude: - - 'lib/secure_headers/headers/cookie.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideBlockBraces: - Exclude: - - 'spec/lib/secure_headers/view_helpers_spec.rb' - -# Offense count: 49 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces. -# SupportedStyles: space, no_space, compact -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideHashLiteralBraces: - Exclude: - - 'spec/lib/secure_headers/configuration_spec.rb' - - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' - - 'spec/lib/secure_headers/headers/cookie_spec.rb' - - 'spec/lib/secure_headers/middleware_spec.rb' - - 'spec/lib/secure_headers_spec.rb' - -# Offense count: 3 -# Configuration parameters: AllowSafeAssignment. -Lint/AssignmentInCondition: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/middleware.rb' - -# Offense count: 2 -Lint/EmptyWhen: - Exclude: - - 'lib/secure_headers/headers/clear_site_data.rb' - -# Offense count: 11 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleAlignWith, SupportedStylesAlignWith, AutoCorrect. -# SupportedStylesAlignWith: keyword, variable, start_of_line -Lint/EndAlignment: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/configuration.rb' - - 'lib/secure_headers/headers/content_security_policy.rb' - - 'lib/secure_headers/view_helper.rb' - - 'spec/lib/secure_headers/configuration_spec.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - -# Offense count: 3 -Lint/ParenthesesAsGroupedExpression: - Exclude: - - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' - - 'spec/lib/secure_headers/headers/policy_management_spec.rb' - -# Offense count: 74 -# Cop supports --auto-correct. -Lint/PercentStringArray: - Exclude: - - 'lib/secure_headers/headers/content_security_policy_config.rb' - - 'spec/lib/secure_headers/configuration_spec.rb' - - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' - - 'spec/lib/secure_headers/headers/policy_management_spec.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - - 'spec/lib/secure_headers_spec.rb' - -# Offense count: 1 -Lint/ScriptPermission: - Exclude: - - 'Rakefile' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. -Lint/UnusedBlockArgument: - Exclude: - - 'lib/tasks/tasks.rake' - - 'spec/lib/secure_headers_spec.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. -Lint/UnusedMethodArgument: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/configuration.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - -# Offense count: 16 -Metrics/AbcSize: - Max: 32 - -# Offense count: 30 -# Configuration parameters: CountComments, ExcludedMethods. -Metrics/BlockLength: - Max: 518 - -# Offense count: 2 -# Configuration parameters: CountComments. -Metrics/ClassLength: - Max: 219 - -# Offense count: 9 -Metrics/CyclomaticComplexity: - Max: 8 - -# Offense count: 367 -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. -# URISchemes: http, https -Metrics/LineLength: - Max: 504 - -# Offense count: 19 -# Configuration parameters: CountComments. -Metrics/MethodLength: - Max: 26 - -# Offense count: 7 -# Configuration parameters: CountComments. -Metrics/ModuleLength: - Max: 520 - -# Offense count: 9 -Metrics/PerceivedComplexity: - Max: 10 - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: MaxKeyValuePairs. -Performance/RedundantMerge: - Exclude: - - 'lib/secure_headers/middleware.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Performance/StringReplacement: - Exclude: - - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' - -# Offense count: 6 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: prefer_alias, prefer_alias_method -Style/Alias: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/configuration.rb' - - 'lib/secure_headers/headers/content_security_policy_config.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods. -# SupportedStyles: line_count_based, semantic, braces_for_chaining -# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object -# FunctionalMethods: let, let!, subject, watch -# IgnoredMethods: lambda, proc, it -Style/BlockDelimiters: - Exclude: - - 'spec/lib/secure_headers/view_helpers_spec.rb' - - 'spec/lib/secure_headers_spec.rb' - -# Offense count: 8 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: braces, no_braces, context_dependent -Style/BracesAroundHashParameters: - Exclude: - - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' - - 'spec/lib/secure_headers_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: Keywords. -# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW -Style/CommentAnnotation: - Exclude: - - 'lib/tasks/tasks.rake' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. -# SupportedStyles: assign_to_condition, assign_inside_condition -Style/ConditionalAssignment: - Exclude: - - 'lib/secure_headers/headers/content_security_policy.rb' - -# Offense count: 24 -Style/Documentation: - Enabled: false - -# Offense count: 2 -Style/DoubleNegation: - Exclude: - - 'lib/secure_headers/headers/public_key_pins.rb' - -# Offense count: 42 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: when_needed, always, never -Style/FrozenStringLiteralComment: - Enabled: false - -# Offense count: 23 -# Configuration parameters: MinBodyLength. -Style/GuardClause: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/configuration.rb' - - 'lib/secure_headers/headers/content_security_policy_config.rb' - - 'lib/secure_headers/headers/policy_management.rb' - - 'lib/secure_headers/headers/public_key_pins.rb' - - 'lib/secure_headers/headers/referrer_policy.rb' - - 'lib/secure_headers/headers/x_content_type_options.rb' - - 'lib/secure_headers/headers/x_download_options.rb' - - 'lib/secure_headers/headers/x_frame_options.rb' - - 'lib/secure_headers/headers/x_permitted_cross_domain_policies.rb' - - 'lib/secure_headers/middleware.rb' - - 'lib/secure_headers/utils/cookies_config.rb' - -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. -# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -Style/HashSyntax: - Exclude: - - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - - 'spec/lib/secure_headers_spec.rb' - -# Offense count: 2 -Style/IfInsideElse: - Exclude: - - 'lib/secure_headers/configuration.rb' - -# Offense count: 12 -# Cop supports --auto-correct. -# Configuration parameters: MaxLineLength. -Style/IfUnlessModifier: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/headers/content_security_policy.rb' - - 'lib/secure_headers/headers/content_security_policy_config.rb' - - 'lib/secure_headers/headers/policy_management.rb' - - 'lib/secure_headers/middleware.rb' - - 'lib/tasks/tasks.rake' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: line_count_dependent, lambda, literal -Style/Lambda: - Exclude: - - 'spec/lib/secure_headers/middleware_spec.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - -# Offense count: 1 -Style/MultilineBlockChain: - Exclude: - - 'lib/secure_headers/headers/policy_management.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/MultilineIfModifier: - Exclude: - - 'lib/secure_headers/view_helper.rb' - -# Offense count: 1 -Style/MultipleComparison: - Exclude: - - 'lib/secure_headers.rb' - -# Offense count: 22 -# Cop supports --auto-correct. -Style/MutableConstant: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/configuration.rb' - - 'lib/secure_headers/headers/clear_site_data.rb' - - 'lib/secure_headers/headers/content_security_policy_config.rb' - - 'lib/secure_headers/headers/policy_management.rb' - - 'lib/secure_headers/headers/referrer_policy.rb' - - 'lib/secure_headers/headers/strict_transport_security.rb' - - 'lib/secure_headers/headers/x_content_type_options.rb' - - 'lib/secure_headers/headers/x_download_options.rb' - - 'lib/secure_headers/headers/x_frame_options.rb' - - 'lib/secure_headers/headers/x_permitted_cross_domain_policies.rb' - - 'lib/secure_headers/headers/x_xss_protection.rb' - - 'lib/secure_headers/middleware.rb' - - 'lib/secure_headers/view_helper.rb' - - 'spec/spec_helper.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: both, prefix, postfix -Style/NegatedIf: - Exclude: - - 'lib/secure_headers/utils/cookies_config.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. -# SupportedStyles: skip_modifier_ifs, always -Style/Next: - Exclude: - - 'lib/secure_headers.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: Strict. -Style/NumericLiterals: - MinDigits: 9 - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Exclude: - - 'spec/**/*' - - 'lib/secure_headers/headers/x_content_type_options.rb' - - 'lib/secure_headers/headers/x_download_options.rb' - -# Offense count: 1 -Style/OpMethod: - Exclude: - - 'lib/secure_headers/headers/content_security_policy_config.rb' - -# Offense count: 134 -# Cop supports --auto-correct. -# Configuration parameters: PreferredDelimiters. -Style/PercentLiteralDelimiters: - Exclude: - - 'lib/secure_headers/headers/content_security_policy_config.rb' - - 'lib/secure_headers/headers/policy_management.rb' - - 'lib/secure_headers/headers/referrer_policy.rb' - - 'lib/secure_headers/headers/x_permitted_cross_domain_policies.rb' - - 'spec/lib/secure_headers/configuration_spec.rb' - - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' - - 'spec/lib/secure_headers/headers/policy_management_spec.rb' - - 'spec/lib/secure_headers/middleware_spec.rb' - - 'spec/lib/secure_headers/view_helpers_spec.rb' - - 'spec/lib/secure_headers_spec.rb' - -# Offense count: 4 -# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. -# NamePrefix: is_, has_, have_ -# NamePrefixBlacklist: is_, has_, have_ -# NameWhitelist: is_a? -Style/PredicateName: - Exclude: - - 'spec/**/*' - - 'lib/secure_headers/utils/cookies_config.rb' - - 'lib/tasks/tasks.rake' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: short, verbose -Style/PreferredHashMethods: - Exclude: - - 'lib/secure_headers/headers/cookie.rb' - -# Offense count: 38 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: compact, exploded -Style/RaiseArgs: - Exclude: - - 'lib/secure_headers.rb' - - 'lib/secure_headers/headers/clear_site_data.rb' - - 'lib/secure_headers/headers/policy_management.rb' - - 'lib/secure_headers/headers/public_key_pins.rb' - - 'lib/secure_headers/headers/referrer_policy.rb' - - 'lib/secure_headers/headers/strict_transport_security.rb' - - 'lib/secure_headers/headers/x_content_type_options.rb' - - 'lib/secure_headers/headers/x_download_options.rb' - - 'lib/secure_headers/headers/x_frame_options.rb' - - 'lib/secure_headers/headers/x_permitted_cross_domain_policies.rb' - - 'lib/secure_headers/headers/x_xss_protection.rb' - - 'lib/secure_headers/utils/cookies_config.rb' - - 'lib/secure_headers/view_helper.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/RedundantParentheses: - Exclude: - - 'lib/secure_headers/configuration.rb' - -# Offense count: 15 -# Cop supports --auto-correct. -Style/RedundantSelf: - Exclude: - - 'lib/secure_headers/configuration.rb' - - 'lib/secure_headers/headers/content_security_policy_config.rb' - - 'lib/secure_headers/view_helper.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. -# SupportedStyles: slashes, percent_r, mixed -Style/RegexpLiteral: - Exclude: - - 'lib/tasks/tasks.rake' - -# Offense count: 544 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiterals: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiteralsInInterpolation: - Exclude: - - 'spec/lib/secure_headers/headers/content_security_policy_spec.rb' - -# Offense count: 11 -# Cop supports --auto-correct. -# Configuration parameters: MinSize, SupportedStyles. -# SupportedStyles: percent, brackets -Style/SymbolArray: - EnforcedStyle: brackets - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInLiteral: - Exclude: - - 'lib/secure_headers/headers/cookie.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. -# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym -Style/TrivialAccessors: - Exclude: - - 'lib/secure_headers/configuration.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: SupportedStyles, WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - EnforcedStyle: percent - MinSize: 3 diff --git a/Gemfile b/Gemfile index a4f9f678..f69e329f 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,8 @@ group :test do gem "json", "~> 1" gem "rack", "~> 1" gem "rspec" - gem "rubocop" + gem "rubocop", "~> 0.47.0" + gem "rubocop-github" gem "coveralls" gem "term-ansicolor", "< 1.4" end From e21ab51db5ddf40762186d5cd4ffa5b21496a3db Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 1 Jun 2017 21:56:25 -0400 Subject: [PATCH 371/636] fix rubocop violations --- Gemfile | 9 +- Guardfile | 1 + Rakefile | 25 +++--- lib/secure_headers.rb | 1 + lib/secure_headers/configuration.rb | 3 +- lib/secure_headers/hash_helper.rb | 3 +- lib/secure_headers/headers/clear_site_data.rb | 3 +- .../headers/content_security_policy.rb | 9 +- .../headers/content_security_policy_config.rb | 1 + lib/secure_headers/headers/cookie.rb | 7 +- .../headers/policy_management.rb | 1 + lib/secure_headers/headers/public_key_pins.rb | 7 +- lib/secure_headers/headers/referrer_policy.rb | 1 + .../headers/strict_transport_security.rb | 3 +- .../headers/x_content_type_options.rb | 1 + .../headers/x_download_options.rb | 3 +- lib/secure_headers/headers/x_frame_options.rb | 1 + .../x_permitted_cross_domain_policies.rb | 3 +- .../headers/x_xss_protection.rb | 3 +- lib/secure_headers/middleware.rb | 19 ++-- lib/secure_headers/railtie.rb | 13 +-- lib/secure_headers/utils/cookies_config.rb | 5 +- lib/secure_headers/view_helper.rb | 3 +- lib/tasks/tasks.rake | 3 +- secure_headers.gemspec | 3 +- spec/lib/secure_headers/configuration_spec.rb | 3 +- .../headers/clear_site_data_spec.rb | 3 +- .../headers/content_security_policy_spec.rb | 5 +- .../lib/secure_headers/headers/cookie_spec.rb | 3 +- .../headers/policy_management_spec.rb | 3 +- .../headers/public_key_pins_spec.rb | 13 +-- .../headers/referrer_policy_spec.rb | 7 +- .../headers/strict_transport_security_spec.rb | 9 +- .../headers/x_content_type_options_spec.rb | 3 +- .../headers/x_download_options_spec.rb | 5 +- .../headers/x_frame_options_spec.rb | 3 +- .../x_permitted_cross_domain_policies_spec.rb | 7 +- .../headers/x_xss_protection_spec.rb | 7 +- spec/lib/secure_headers/middleware_spec.rb | 21 ++--- spec/lib/secure_headers/view_helpers_spec.rb | 9 +- spec/lib/secure_headers_spec.rb | 89 ++++++++++--------- spec/spec_helper.rb | 19 ++-- 42 files changed, 191 insertions(+), 149 deletions(-) diff --git a/Gemfile b/Gemfile index f69e329f..b4565c97 100644 --- a/Gemfile +++ b/Gemfile @@ -1,21 +1,22 @@ +# frozen_string_literal: true source "https://rubygems.org" gemspec group :test do - gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0 - gem "pry-nav" + gem "coveralls" gem "json", "~> 1" + gem "pry-nav" gem "rack", "~> 1" gem "rspec" gem "rubocop", "~> 0.47.0" gem "rubocop-github" - gem "coveralls" gem "term-ansicolor", "< 1.4" + gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0 end group :guard do - gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] gem "growl" + gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] gem "rb-fsevent" end diff --git a/Guardfile b/Guardfile index f3d4ebd9..18c162fa 100644 --- a/Guardfile +++ b/Guardfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true guard :rspec, cmd: "bundle exec rspec", all_on_start: true, all_after_pass: true do require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) diff --git a/Rakefile b/Rakefile index 910d7957..1b956fd0 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,9 @@ #!/usr/bin/env rake -require 'bundler/gem_tasks' -require 'rspec/core/rake_task' -require 'net/http' -require 'net/https' +# frozen_string_literal: true +require "bundler/gem_tasks" +require "rspec/core/rake_task" +require "net/http" +require "net/https" desc "Run RSpec" # RSpec::Core::RakeTask.new(:spec) @@ -12,25 +13,25 @@ RSpec::Core::RakeTask.new do |t| end begin - require 'rdoc/task' + require "rdoc/task" rescue LoadError - require 'rdoc/rdoc' - require 'rake/rdoctask' + require "rdoc/rdoc" + require "rake/rdoctask" RDoc::Task = Rake::RDocTask end begin - require 'rubocop/rake_task' + require "rubocop/rake_task" RuboCop::RakeTask.new rescue LoadError task(:rubocop) { $stderr.puts "RuboCop is disabled" } end RDoc::Task.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'SecureHeaders' - rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('lib/**/*.rb') + rdoc.rdoc_dir = "rdoc" + rdoc.title = "SecureHeaders" + rdoc.options << "--line-numbers" + rdoc.rdoc_files.include("lib/**/*.rb") end task default: [:spec, :rubocop] diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 86971523..5f0349e5 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require "secure_headers/configuration" require "secure_headers/hash_helper" require "secure_headers/headers/cookie" diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 99777347..bdadc6c0 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -1,4 +1,5 @@ -require 'yaml' +# frozen_string_literal: true +require "yaml" module SecureHeaders class Configuration diff --git a/lib/secure_headers/hash_helper.rb b/lib/secure_headers/hash_helper.rb index 9f591709..2b007a3e 100644 --- a/lib/secure_headers/hash_helper.rb +++ b/lib/secure_headers/hash_helper.rb @@ -1,4 +1,5 @@ -require 'base64' +# frozen_string_literal: true +require "base64" module SecureHeaders module HashHelper diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index c078caab..b164c904 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders class ClearSiteDataConfigError < StandardError; end class ClearSiteData @@ -17,7 +18,7 @@ class << self # Public: make an Clear-Site-Data header name, value pair # # Returns nil if not configured, returns header name and value if configured. - def make_header(config=nil) + def make_header(config = nil) case config when nil, OPT_OUT, [] # noop diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 8e23e12d..7be6a859 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -1,6 +1,7 @@ -require_relative 'policy_management' -require_relative 'content_security_policy_config' -require 'useragent' +# frozen_string_literal: true +require_relative "policy_management" +require_relative "content_security_policy_config" +require "useragent" module SecureHeaders class ContentSecurityPolicy @@ -229,7 +230,7 @@ def nonces_supported? end def symbol_to_hyphen_case(sym) - sym.to_s.tr('_', '-') + sym.to_s.tr("_", "-") end end end diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 3b8a1473..3ac2ddc1 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders module DynamicConfig def self.included(base) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index b3423395..2c683d1c 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -1,5 +1,6 @@ -require 'cgi' -require 'secure_headers/utils/cookies_config' +# frozen_string_literal: true +require "cgi" +require "secure_headers/utils/cookies_config" module SecureHeaders class CookiesConfigError < StandardError; end @@ -113,7 +114,7 @@ def parse(cookie) return unless cookie cookie.split(/[;,]\s?/).each do |pairs| - name, values = pairs.split('=',2) + name, values = pairs.split("=", 2) name = CGI.unescape(name) attribute = name.downcase.to_sym diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 91b62353..4512e63b 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders module PolicyManagement def self.included(base) diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb index 943f41a6..7734f932 100644 --- a/lib/secure_headers/headers/public_key_pins.rb +++ b/lib/secure_headers/headers/public_key_pins.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders class PublicKeyPinsConfigError < StandardError; end class PublicKeyPins @@ -54,7 +55,7 @@ def value pin_directives, report_uri_directive, subdomain_directive - ].compact.join('; ').strip + ].compact.join("; ").strip end def pin_directives @@ -63,7 +64,7 @@ def pin_directives pin.map do |token, hash| "pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token) end - end.join('; ') + end.join("; ") end def max_age_directive @@ -75,7 +76,7 @@ def report_uri_directive end def subdomain_directive - @include_subdomains ? 'includeSubDomains' : nil + @include_subdomains ? "includeSubDomains" : nil end end end diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index d7366c6c..7cd69479 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders class ReferrerPolicyConfigError < StandardError; end class ReferrerPolicy diff --git a/lib/secure_headers/headers/strict_transport_security.rb b/lib/secure_headers/headers/strict_transport_security.rb index 25269d93..ccb58030 100644 --- a/lib/secure_headers/headers/strict_transport_security.rb +++ b/lib/secure_headers/headers/strict_transport_security.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true module SecureHeaders class STSConfigError < StandardError; end class StrictTransportSecurity - HEADER_NAME = 'Strict-Transport-Security'.freeze + HEADER_NAME = "Strict-Transport-Security".freeze HSTS_MAX_AGE = "631138519" DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index a144cc85..bc30ba65 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders class XContentTypeOptionsConfigError < StandardError; end diff --git a/lib/secure_headers/headers/x_download_options.rb b/lib/secure_headers/headers/x_download_options.rb index e3a96cf0..076a682e 100644 --- a/lib/secure_headers/headers/x_download_options.rb +++ b/lib/secure_headers/headers/x_download_options.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true module SecureHeaders class XDOConfigError < StandardError; end class XDownloadOptions HEADER_NAME = "X-Download-Options".freeze - DEFAULT_VALUE = 'noopen' + DEFAULT_VALUE = "noopen" CONFIG_KEY = :x_download_options class << self diff --git a/lib/secure_headers/headers/x_frame_options.rb b/lib/secure_headers/headers/x_frame_options.rb index c8d7e654..863e4e56 100644 --- a/lib/secure_headers/headers/x_frame_options.rb +++ b/lib/secure_headers/headers/x_frame_options.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders class XFOConfigError < StandardError; end class XFrameOptions diff --git a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb index c153d844..73e099ae 100644 --- a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +++ b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true module SecureHeaders class XPCDPConfigError < StandardError; end class XPermittedCrossDomainPolicies HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze - DEFAULT_VALUE = 'none' + DEFAULT_VALUE = "none" VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) CONFIG_KEY = :x_permitted_cross_domain_policies diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index a542b8ae..edeefda1 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true module SecureHeaders class XXssProtectionConfigError < StandardError; end class XXssProtection - HEADER_NAME = 'X-XSS-Protection'.freeze + HEADER_NAME = "X-XSS-Protection".freeze DEFAULT_VALUE = "1; mode=block" VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i CONFIG_KEY = :x_xss_protection diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 20f59e55..52cc1319 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders class Middleware HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166" @@ -25,11 +26,11 @@ def call(env) # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194 def flag_cookies!(headers, config) - if cookies = headers['Set-Cookie'] + if cookies = headers["Set-Cookie"] # Support Rails 2.3 / Rack 1.1 arrays as headers cookies = cookies.split("\n") unless cookies.is_a?(Array) - headers['Set-Cookie'] = cookies.map do |cookie| + headers["Set-Cookie"] = cookies.map do |cookie| SecureHeaders::Cookie.new(cookie, config).to_s end.join("\n") end @@ -37,8 +38,8 @@ def flag_cookies!(headers, config) # disable Secure cookies for non-https requests def override_secure(env, config = {}) - if scheme(env) != 'https' - config.merge!(secure: false) + if scheme(env) != "https" + config[:secure] = false end config @@ -46,12 +47,12 @@ def override_secure(env, config = {}) # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119 def scheme(env) - if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on' - 'https' - elsif env['HTTP_X_FORWARDED_PROTO'] - env['HTTP_X_FORWARDED_PROTO'].split(',')[0] + if env["HTTPS"] == "on" || env["HTTP_X_SSL_REQUEST"] == "on" + "https" + elsif env["HTTP_X_FORWARDED_PROTO"] + env["HTTP_X_FORWARDED_PROTO"].split(",")[0] else - env['rack.url_scheme'] + env["rack.url_scheme"] end end end diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index 03df7e87..53c3aec6 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -1,20 +1,21 @@ +# frozen_string_literal: true # rails 3.1+ if defined?(Rails::Railtie) module SecureHeaders class Railtie < Rails::Railtie isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0 - conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', - 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options', - 'X-Content-Type-Options', 'Strict-Transport-Security', - 'Content-Security-Policy', 'Content-Security-Policy-Report-Only', - 'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy'] + conflicting_headers = ["X-Frame-Options", "X-XSS-Protection", + "X-Permitted-Cross-Domain-Policies", "X-Download-Options", + "X-Content-Type-Options", "Strict-Transport-Security", + "Content-Security-Policy", "Content-Security-Policy-Report-Only", + "Public-Key-Pins", "Public-Key-Pins-Report-Only", "Referrer-Policy"] initializer "secure_headers.middleware" do Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware end rake_tasks do - load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__)) + load File.expand_path(File.join("..", "..", "lib", "tasks", "tasks.rake"), File.dirname(__FILE__)) end initializer "secure_headers.action_controller" do diff --git a/lib/secure_headers/utils/cookies_config.rb b/lib/secure_headers/utils/cookies_config.rb index ebf89ac2..707feadc 100644 --- a/lib/secure_headers/utils/cookies_config.rb +++ b/lib/secure_headers/utils/cookies_config.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders class CookiesConfig @@ -51,10 +52,10 @@ def validate_samesite_boolean_config! def validate_samesite_hash_config! # validate Hash-based samesite configuration if is_hash?(config[:samesite][:lax]) - validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], 'samesite lax') + validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], "samesite lax") if is_hash?(config[:samesite][:strict]) - validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict') + validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], "samesite strict") validate_exclusive_use_of_samesite_enforcement!(:only) validate_exclusive_use_of_samesite_enforcement!(:except) end diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index b4059247..5353c518 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SecureHeaders module ViewHelpers include SecureHeaders::HashHelper @@ -75,7 +76,7 @@ def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block) end content = capture(&block) - file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb') + file_path = File.join("app", "views", self.instance_variable_get(:@virtual_path) + ".html.erb") if raise_error_on_unrecognized_hash hash_value = hash_source(content) diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake index 1be2a81b..374fea5e 100644 --- a/lib/tasks/tasks.rake +++ b/lib/tasks/tasks.rake @@ -1,3 +1,4 @@ +# frozen_string_literal: true INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX INLINE_STYLE_REGEX = /(]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX @@ -73,7 +74,7 @@ namespace :secure_headers do end end - File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file| + File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, "w") do |file| file.write(script_hashes.to_yaml) end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index e910a820..e60d38f9 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,10 +1,11 @@ # -*- encoding: utf-8 -*- +# frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" gem.version = "3.6.4" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] - gem.description = 'Manages application of security headers with many safe defaults.' + gem.description = "Manages application of security headers with many safe defaults." gem.summary = 'Add easily configured security headers to responses including content-security-policy, x-frame-options, strict-transport-security, etc.' diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 40b285e5..3e161221 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe Configuration do diff --git a/spec/lib/secure_headers/headers/clear_site_data_spec.rb b/spec/lib/secure_headers/headers/clear_site_data_spec.rb index e065583c..3fe4f2a1 100644 --- a/spec/lib/secure_headers/headers/clear_site_data_spec.rb +++ b/spec/lib/secure_headers/headers/clear_site_data_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe ClearSiteData do diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 1570f09f..f2f84315 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe ContentSecurityPolicy do @@ -52,7 +53,7 @@ module SecureHeaders end it "does not remove schemes when :preserve_schemes is true" do - csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :preserve_schemes => true) + csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), preserve_schemes: true) expect(csp.value).to eq("default-src https://example.org") end diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index 15f28076..bdb84b1b 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe Cookie do diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index f711d5f0..1d3a2ef3 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe PolicyManagement do diff --git a/spec/lib/secure_headers/headers/public_key_pins_spec.rb b/spec/lib/secure_headers/headers/public_key_pins_spec.rb index 5f6d68f6..31e661e0 100644 --- a/spec/lib/secure_headers/headers/public_key_pins_spec.rb +++ b/spec/lib/secure_headers/headers/public_key_pins_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe PublicKeyPins do @@ -8,7 +9,7 @@ module SecureHeaders specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } specify do - config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin1' }, { sha256: 'base64encodedpin2' }] } + config = { max_age: 1234, pins: [{ sha256: "base64encodedpin1" }, { sha256: "base64encodedpin2" }] } header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\"" expect(PublicKeyPins.new(config).value).to eq(header_value) end @@ -16,19 +17,19 @@ module SecureHeaders context "with an invalid configuration" do it "raises an exception when max-age is not provided" do expect do - PublicKeyPins.validate_config!(foo: 'bar') + PublicKeyPins.validate_config!(foo: "bar") end.to raise_error(PublicKeyPinsConfigError) end it "raises an exception with an invalid max-age" do expect do - PublicKeyPins.validate_config!(max_age: 'abc123') + PublicKeyPins.validate_config!(max_age: "abc123") end.to raise_error(PublicKeyPinsConfigError) end - it 'raises an exception with less than 2 pins' do + it "raises an exception with less than 2 pins" do expect do - config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin' }] } + config = { max_age: 1234, pins: [{ sha256: "base64encodedpin" }] } PublicKeyPins.validate_config!(config) end.to raise_error(PublicKeyPinsConfigError) end diff --git a/spec/lib/secure_headers/headers/referrer_policy_spec.rb b/spec/lib/secure_headers/headers/referrer_policy_spec.rb index 35f9f824..96dbd62d 100644 --- a/spec/lib/secure_headers/headers/referrer_policy_spec.rb +++ b/spec/lib/secure_headers/headers/referrer_policy_spec.rb @@ -1,9 +1,10 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe ReferrerPolicy do specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) } - specify { expect(ReferrerPolicy.make_header('no-referrer')).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) } + specify { expect(ReferrerPolicy.make_header("no-referrer")).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) } context "valid configuration values" do it "accepts 'no-referrer'" do @@ -61,7 +62,7 @@ module SecureHeaders end end - context 'invlaid configuration values' do + context "invlaid configuration values" do it "doesn't accept invalid values" do expect do ReferrerPolicy.validate_config!("open") diff --git a/spec/lib/secure_headers/headers/strict_transport_security_spec.rb b/spec/lib/secure_headers/headers/strict_transport_security_spec.rb index 346c7080..23acbf0d 100644 --- a/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +++ b/spec/lib/secure_headers/headers/strict_transport_security_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe StrictTransportSecurity do @@ -10,19 +11,19 @@ module SecureHeaders context "with a string argument" do it "raises an exception with an invalid max-age" do expect do - StrictTransportSecurity.validate_config!('max-age=abc123') + StrictTransportSecurity.validate_config!("max-age=abc123") end.to raise_error(STSConfigError) end it "raises an exception if max-age is not supplied" do expect do - StrictTransportSecurity.validate_config!('includeSubdomains') + StrictTransportSecurity.validate_config!("includeSubdomains") end.to raise_error(STSConfigError) end it "raises an exception with an invalid format" do expect do - StrictTransportSecurity.validate_config!('max-age=123includeSubdomains') + StrictTransportSecurity.validate_config!("max-age=123includeSubdomains") end.to raise_error(STSConfigError) end end diff --git a/spec/lib/secure_headers/headers/x_content_type_options_spec.rb b/spec/lib/secure_headers/headers/x_content_type_options_spec.rb index face9f84..70d1029b 100644 --- a/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +++ b/spec/lib/secure_headers/headers/x_content_type_options_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe XContentTypeOptions do diff --git a/spec/lib/secure_headers/headers/x_download_options_spec.rb b/spec/lib/secure_headers/headers/x_download_options_spec.rb index 798e4287..8263a6d3 100644 --- a/spec/lib/secure_headers/headers/x_download_options_spec.rb +++ b/spec/lib/secure_headers/headers/x_download_options_spec.rb @@ -1,9 +1,10 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe XDownloadOptions do specify { expect(XDownloadOptions.make_header).to eq([XDownloadOptions::HEADER_NAME, XDownloadOptions::DEFAULT_VALUE]) } - specify { expect(XDownloadOptions.make_header('noopen')).to eq([XDownloadOptions::HEADER_NAME, 'noopen']) } + specify { expect(XDownloadOptions.make_header("noopen")).to eq([XDownloadOptions::HEADER_NAME, "noopen"]) } context "invalid configuration values" do it "accepts noopen" do diff --git a/spec/lib/secure_headers/headers/x_frame_options_spec.rb b/spec/lib/secure_headers/headers/x_frame_options_spec.rb index 1c050d94..8815d8ea 100644 --- a/spec/lib/secure_headers/headers/x_frame_options_spec.rb +++ b/spec/lib/secure_headers/headers/x_frame_options_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe XFrameOptions do diff --git a/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb b/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb index 86744fc2..3cd0fa2b 100644 --- a/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +++ b/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb @@ -1,9 +1,10 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe XPermittedCrossDomainPolicies do specify { expect(XPermittedCrossDomainPolicies.make_header).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, "none"]) } - specify { expect(XPermittedCrossDomainPolicies.make_header('master-only')).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, 'master-only']) } + specify { expect(XPermittedCrossDomainPolicies.make_header("master-only")).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, "master-only"]) } context "valid configuration values" do it "accepts 'all'" do @@ -36,7 +37,7 @@ module SecureHeaders end end - context 'invlaid configuration values' do + context "invlaid configuration values" do it "doesn't accept invalid values" do expect do XPermittedCrossDomainPolicies.validate_config!("open") diff --git a/spec/lib/secure_headers/headers/x_xss_protection_spec.rb b/spec/lib/secure_headers/headers/x_xss_protection_spec.rb index f5fbb8e8..de772756 100644 --- a/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +++ b/spec/lib/secure_headers/headers/x_xss_protection_spec.rb @@ -1,9 +1,10 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe XXssProtection do specify { expect(XXssProtection.make_header).to eq([XXssProtection::HEADER_NAME, XXssProtection::DEFAULT_VALUE]) } - specify { expect(XXssProtection.make_header("1; mode=block; report=https://www.secure.com/reports")).to eq([XXssProtection::HEADER_NAME, '1; mode=block; report=https://www.secure.com/reports']) } + specify { expect(XXssProtection.make_header("1; mode=block; report=https://www.secure.com/reports")).to eq([XXssProtection::HEADER_NAME, "1; mode=block; report=https://www.secure.com/reports"]) } context "with invalid configuration" do it "should raise an error when providing a string that is not valid" do @@ -19,7 +20,7 @@ module SecureHeaders context "when using a hash value" do it "should allow string values ('1' or '0' are the only valid strings)" do expect do - XXssProtection.validate_config!('1') + XXssProtection.validate_config!("1") end.not_to raise_error end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index bfd78ae8..82b759b8 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require "spec_helper" module SecureHeaders @@ -19,8 +20,8 @@ module SecureHeaders config.hpkp = { max_age: 10000000, pins: [ - {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, - {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} + {sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"}, + {sha256: "73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f"} ], report_uri: "https://#{report_host}/example-hpkp" } @@ -62,7 +63,7 @@ module SecureHeaders end request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to eq("foo=bar; secure") + expect(env["Set-Cookie"]).to eq("foo=bar; secure") end end @@ -73,7 +74,7 @@ module SecureHeaders end request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to eq("foo=bar") + expect(env["Set-Cookie"]).to eq("foo=bar") end end end @@ -84,7 +85,7 @@ module SecureHeaders request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to eq("foo=bar; secure; HttpOnly") + expect(env["Set-Cookie"]).to eq("foo=bar; secure; HttpOnly") end it "flags cookies with a combination of SameSite configurations" do @@ -94,8 +95,8 @@ module SecureHeaders request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to match("_session=foobar; SameSite=Strict") - expect(env['Set-Cookie']).to match("_guest=true; SameSite=Lax") + expect(env["Set-Cookie"]).to match("_session=foobar; SameSite=Strict") + expect(env["Set-Cookie"]).to match("_guest=true; SameSite=Lax") end it "disables secure cookies for non-https requests" do @@ -103,7 +104,7 @@ module SecureHeaders request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to eq("foo=bar") + expect(env["Set-Cookie"]).to eq("foo=bar") end it "sets the secure cookie flag correctly on interleaved http/https requests" do @@ -111,11 +112,11 @@ module SecureHeaders request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to eq("foo=bar") + expect(env["Set-Cookie"]).to eq("foo=bar") request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env['Set-Cookie']).to eq("foo=bar; secure") + expect(env["Set-Cookie"]).to eq("foo=bar; secure") end end end diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index d8ab1629..5fae59b6 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require "spec_helper" require "erb" @@ -58,7 +59,7 @@ def content_tag(type, content = nil, options = nil, &block) end if options.is_a?(Hash) - options = options.map {|k,v| " #{k}=#{v}"} + options = options.map {|k, v| " #{k}=#{v}"} end "<#{type}#{options}>#{content}" end @@ -82,9 +83,9 @@ module SecureHeaders before(:all) do Configuration.default do |config| config.csp = { - :default_src => %w('self'), - :script_src => %w('self'), - :style_src => %w('self') + default_src: %w('self'), + script_src: %w('self'), + style_src: %w('self') } end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index f5e330ab..86aca37f 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" module SecureHeaders describe SecureHeaders do @@ -37,9 +38,9 @@ module SecureHeaders SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY) SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy-Report-Only']).to be_nil - expect(hash['Content-Security-Policy']).to be_nil - expect(hash['X-Content-Type-Options']).to be_nil + expect(hash["Content-Security-Policy-Report-Only"]).to be_nil + expect(hash["Content-Security-Policy"]).to be_nil + expect(hash["X-Content-Type-Options"]).to be_nil end it "Carries options over when using overrides" do @@ -54,15 +55,15 @@ module SecureHeaders SecureHeaders.use_secure_headers_override(request, :api) hash = SecureHeaders.header_hash_for(request) - expect(hash['X-Download-Options']).to be_nil - expect(hash['X-Permitted-Cross-Domain-Policies']).to be_nil - expect(hash['X-Frame-Options']).to be_nil + expect(hash["X-Download-Options"]).to be_nil + expect(hash["X-Permitted-Cross-Domain-Policies"]).to be_nil + expect(hash["X-Frame-Options"]).to be_nil end it "allows you to opt out entirely" do # configure the disabled-by-default headers to ensure they also do not get set Configuration.default do |config| - config.csp = { :default_src => ["example.com"] } + config.csp = { default_src: ["example.com"] } config.csp_report_only = config.csp config.hpkp = { report_only: false, @@ -144,10 +145,10 @@ module SecureHeaders config.hpkp = { max_age: 1_000_000, include_subdomains: true, - report_uri: '//example.com/uri-directive', + report_uri: "//example.com/uri-directive", pins: [ - { sha256: 'abc' }, - { sha256: '123' } + { sha256: "abc" }, + { sha256: "123" } ] } end @@ -301,7 +302,7 @@ module SecureHeaders SecureHeaders.content_security_policy_script_nonce(chrome_request) hash = SecureHeaders.header_hash_for(chrome_request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") end it "uses a nonce for safari 10+" do @@ -315,7 +316,7 @@ module SecureHeaders safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10])) nonce = SecureHeaders.content_security_policy_script_nonce(safari_request) hash = SecureHeaders.header_hash_for(safari_request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'") end it "supports the deprecated `report_only: true` format" do @@ -368,8 +369,8 @@ module SecureHeaders end hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") end it "sets different headers when the configs are different" do @@ -381,8 +382,8 @@ module SecureHeaders end hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "allows you to opt-out of enforced CSP" do @@ -394,8 +395,8 @@ module SecureHeaders end hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to be_nil - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to be_nil + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") end it "opts-out of enforced CSP when only csp_report_only is set" do @@ -407,8 +408,8 @@ module SecureHeaders end hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to be_nil - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to be_nil + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") end it "allows you to set csp_report_only before csp" do @@ -421,50 +422,50 @@ module SecureHeaders end hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") end it "allows appending to the enforced policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") end it "allows appending to the report only policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows appending to both policies" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows overriding the enforced policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") end it "allows overriding the report only policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com") end it "allows overriding both policies" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com") end context "when inferring which config to modify" do @@ -477,8 +478,8 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash['Content-Security-Policy-Report-Only']).to be_nil + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["Content-Security-Policy-Report-Only"]).to be_nil end it "updates the report only header when configured" do @@ -491,8 +492,8 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash['Content-Security-Policy']).to be_nil + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["Content-Security-Policy"]).to be_nil end it "updates both headers if both are configured" do @@ -507,8 +508,8 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) - expect(hash['Content-Security-Policy']).to eq("default-src enforced.com; script-src enforced.com anothercdn.com") - expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src reportonly.com; script-src reportonly.com anothercdn.com") + expect(hash["Content-Security-Policy"]).to eq("default-src enforced.com; script-src enforced.com anothercdn.com") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src reportonly.com; script-src reportonly.com anothercdn.com") end end @@ -520,7 +521,7 @@ module SecureHeaders it "validates your hsts config upon configuration" do expect do Configuration.default do |config| - config.hsts = 'lol' + config.hsts = "lol" end end.to raise_error(STSConfigError) end @@ -528,7 +529,7 @@ module SecureHeaders it "validates your csp config upon configuration" do expect do Configuration.default do |config| - config.csp = { ContentSecurityPolicy::DEFAULT_SRC => '123456' } + config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" } end end.to raise_error(ContentSecurityPolicyConfigError) end @@ -536,7 +537,7 @@ module SecureHeaders it "raises errors for unknown directives" do expect do Configuration.default do |config| - config.csp = { made_up_directive: '123456' } + config.csp = { made_up_directive: "123456" } end end.to raise_error(ContentSecurityPolicyConfigError) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6842c9bc..62c74ec9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,12 +1,13 @@ -require 'rubygems' -require 'rspec' -require 'rack' -require 'pry-nav' +# frozen_string_literal: true +require "rubygems" +require "rspec" +require "rack" +require "pry-nav" -require 'coveralls' +require "coveralls" Coveralls.wear! -require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers') +require File.join(File.dirname(__FILE__), "..", "lib", "secure_headers") @@ -14,9 +15,9 @@ edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1", firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0", - chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5', - ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)', - opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00', + chrome: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5", + ie: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)", + opera: "Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00", ios5: "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", ios6: "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25", safari5: "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3", From 886a322880200807d8075ff156957fdc9849640c Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Fri, 2 Jun 2017 15:40:21 -0400 Subject: [PATCH 372/636] remove rspec block notation from Rakefile --- .rspec | 1 + Rakefile | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.rspec b/.rspec index 32fe547d..4de3a1c7 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,3 @@ --order rand --warnings +--format progress diff --git a/Rakefile b/Rakefile index 1b956fd0..58c9dc94 100644 --- a/Rakefile +++ b/Rakefile @@ -5,12 +5,7 @@ require "rspec/core/rake_task" require "net/http" require "net/https" -desc "Run RSpec" -# RSpec::Core::RakeTask.new(:spec) -RSpec::Core::RakeTask.new do |t| - t.verbose = false - t.rspec_opts = "--format progress" -end +RSpec::Core::RakeTask.new begin require "rdoc/task" From f186589288d87eb4068a4b2c452bd2977b0d9d39 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 20 Jun 2017 07:42:02 -1000 Subject: [PATCH 373/636] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4477109..cfafd622 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ Here are a few things you can do that will increase the likelihood of your pull 0. Ensure CI is green 0. Pull the latest code 0. Increment the version -0. Run `gem build darrrr.gemspec` +0. Run `gem build secure_headers.gemspec` 0. Bump the Gemfile and Gemfile.lock versions for an app which relies on this gem 0. Test behavior locally, branch deploy, whatever needs to happen 0. Run `bundle exec rake release` From f203dc340d9b68b75467fd57d537a1afbfb15327 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 20 Jun 2017 10:35:24 -0600 Subject: [PATCH 374/636] Update Clear-Site-Data to use the current spec format. --- CHANGELOG.md | 4 +++ README.md | 2 +- lib/secure_headers/headers/clear_site_data.rb | 20 ++++++----- .../headers/clear_site_data_spec.rb | 35 +++++-------------- 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1db575e5..4479e2e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.6.5 + +Update clear-site-data header to use current format specified by the specification. + ## 3.6.4 Fix case where mixing frame-src/child-src dynamically would behave in unexpected ways: https://github.com/twitter/secureheaders/pull/325 diff --git a/README.md b/README.md index 2c6127d1..d9a84db5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The gem will automatically apply several headers that are related to security. - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) -- Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://www.w3.org/TR/clear-site-data/). +- Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/). It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so). diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index b164c904..f08a4588 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -23,9 +23,9 @@ def make_header(config = nil) when nil, OPT_OUT, [] # noop when Array - [HEADER_NAME, JSON.dump(TYPES => config)] + [HEADER_NAME, make_header_value(config)] when true - [HEADER_NAME, JSON.dump(TYPES => ALL_TYPES)] + [HEADER_NAME, make_header_value(ALL_TYPES)] end end @@ -37,16 +37,20 @@ def validate_config!(config) unless config.all? { |t| t.is_a?(String) } raise ClearSiteDataConfigError.new("types must be Strings") end - - begin - JSON.dump(config) - rescue JSON::GeneratorError, Encoding::UndefinedConversionError - raise ClearSiteDataConfigError.new("types must serializable by JSON") - end else raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`") end end + + # Public: Transform a Clear-Site-Data config (an Array of String) into a + # String that can be used as the value for the Clear-Site-Data header. + # + # types - An Array of String of types of data to clear. + # + # Returns a String of quoted values that are comma separated. + def make_header_value(types) + types.map { |t| "\"#{t}\""}.join(", ") + end end end end diff --git a/spec/lib/secure_headers/headers/clear_site_data_spec.rb b/spec/lib/secure_headers/headers/clear_site_data_spec.rb index 3fe4f2a1..5262cc7c 100644 --- a/spec/lib/secure_headers/headers/clear_site_data_spec.rb +++ b/spec/lib/secure_headers/headers/clear_site_data_spec.rb @@ -20,30 +20,16 @@ module SecureHeaders name, value = described_class.make_header(true) expect(name).to eq(ClearSiteData::HEADER_NAME) - expect(value).to eq(normalize_json(<<-HERE)) - { - "types": [ - "cache", - "cookies", - "storage", - "executionContexts" - ] - } - HERE + expect(value).to eq( + ClearSiteData.make_header_value(ClearSiteData::ALL_TYPES) + ) end it "returns specified types" do name, value = described_class.make_header(["foo", "bar"]) expect(name).to eq(ClearSiteData::HEADER_NAME) - expect(value).to eq(normalize_json(<<-HERE)) - { - "types": [ - "foo", - "bar" - ] - } - HERE + expect(value).to eq(ClearSiteData.make_header_value(["foo", "bar"])) end end @@ -84,12 +70,6 @@ module SecureHeaders end.to raise_error(ClearSiteDataConfigError) end - it "fails for non-serializable config" do - expect do - described_class.validate_config!(["hi \255"]) - end.to raise_error(ClearSiteDataConfigError) - end - it "fails for other types of config" do expect do described_class.validate_config!(:cookies) @@ -97,8 +77,11 @@ module SecureHeaders end end - def normalize_json(json) - JSON.dump(JSON.parse(json)) + describe "make_header_value" do + it "returns a string of quoted values that are comma separated" do + value = described_class.make_header_value(["foo", "bar"]) + expect(value).to eq(%("foo", "bar")) + end end end end From 9e8c3a3f2cd8103c9a28169cbdc77a1fd0542ab9 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 20 Jun 2017 11:00:25 -0600 Subject: [PATCH 375/636] Remove unused constant. --- lib/secure_headers/headers/clear_site_data.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index f08a4588..c77074e1 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -3,7 +3,6 @@ module SecureHeaders class ClearSiteDataConfigError < StandardError; end class ClearSiteData HEADER_NAME = "Clear-Site-Data".freeze - TYPES = "types".freeze # Valid `types` CACHE = "cache".freeze From e5fd74389bc6e1e012aa53cf7540ec93e7a752ac Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 20 Jun 2017 11:37:36 -0600 Subject: [PATCH 376/636] Bump version number of gem. --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index e60d38f9..36189ada 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "3.6.4" + gem.version = "4.x.x" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 48c69ab33ea376e502a90dd09b42f44878431670 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 20 Jun 2017 11:53:24 -0600 Subject: [PATCH 377/636] Polish based on feedback from @oreoshake. --- lib/secure_headers/headers/clear_site_data.rb | 2 +- .../secure_headers/headers/clear_site_data_spec.rb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index c77074e1..3c7cfc9f 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -41,7 +41,7 @@ def validate_config!(config) end end - # Public: Transform a Clear-Site-Data config (an Array of String) into a + # Public: Transform a Clear-Site-Data config (an Array of Strings) into a # String that can be used as the value for the Clear-Site-Data header. # # types - An Array of String of types of data to clear. diff --git a/spec/lib/secure_headers/headers/clear_site_data_spec.rb b/spec/lib/secure_headers/headers/clear_site_data_spec.rb index 5262cc7c..459061a1 100644 --- a/spec/lib/secure_headers/headers/clear_site_data_spec.rb +++ b/spec/lib/secure_headers/headers/clear_site_data_spec.rb @@ -21,7 +21,7 @@ module SecureHeaders expect(name).to eq(ClearSiteData::HEADER_NAME) expect(value).to eq( - ClearSiteData.make_header_value(ClearSiteData::ALL_TYPES) + %("cache", "cookies", "storage", "executionContexts") ) end @@ -29,7 +29,7 @@ module SecureHeaders name, value = described_class.make_header(["foo", "bar"]) expect(name).to eq(ClearSiteData::HEADER_NAME) - expect(value).to eq(ClearSiteData.make_header_value(["foo", "bar"])) + expect(value).to eq(%("foo", "bar")) end end @@ -78,10 +78,10 @@ module SecureHeaders end describe "make_header_value" do - it "returns a string of quoted values that are comma separated" do - value = described_class.make_header_value(["foo", "bar"]) - expect(value).to eq(%("foo", "bar")) - end + it "returns a string of quoted values that are comma separated" do + value = described_class.make_header_value(["foo", "bar"]) + expect(value).to eq(%("foo", "bar")) + end end end end From 1977637ba3546d20f41ff601326f4096040aee3a Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 28 Jun 2017 19:06:52 -0700 Subject: [PATCH 378/636] Pins dependency version of useragent because secure_headers actually requires this version for the test suite to pass --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 36189ada..a7e064b5 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -16,5 +16,5 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_development_dependency "rake" - gem.add_dependency "useragent" + gem.add_dependency "useragent", ">= 0.15.0" end From 6cf57c09b0edd2f4003fc7c9d96aae0b8535612c Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 11 Jul 2017 22:02:03 -0300 Subject: [PATCH 379/636] Bump Ruby version to 2.4.1 and test versions of 2.3 and 2.4 --- .ruby-version | 2 +- .travis.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ruby-version b/.ruby-version index 0bee604d..005119ba 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.3 +2.4.1 diff --git a/.travis.yml b/.travis.yml index fc379dad..1f092bb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: ruby rvm: - ruby-head - - 2.4.0 - - 2.3.3 + - 2.4.1 + - 2.3.4 - 2.2 - jruby-head From 5b4a5e3be988347fad37be457a86cefe2915403a Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Tue, 11 Jul 2017 22:08:00 -0300 Subject: [PATCH 380/636] Remove dropped "reflected-xss" directive --- lib/secure_headers/headers/content_security_policy_config.rb | 1 - lib/secure_headers/headers/policy_management.rb | 3 --- .../lib/secure_headers/headers/content_security_policy_spec.rb | 1 - 3 files changed, 5 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 3ac2ddc1..0227feca 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -31,7 +31,6 @@ def initialize(hash) @object_src = nil @plugin_types = nil @preserve_schemes = nil - @reflected_xss = nil @report_only = nil @report_uri = nil @sandbox = nil diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 4512e63b..92485f9d 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -64,11 +64,9 @@ def self.included(base) # All the directives currently under consideration for CSP level 3. # https://w3c.github.io/webappsec/specs/CSP2/ MANIFEST_SRC = :manifest_src - REFLECTED_XSS = :reflected_xss DIRECTIVES_3_0 = [ DIRECTIVES_2_0, MANIFEST_SRC, - REFLECTED_XSS ].flatten.freeze # All the directives that are not currently in a formal spec, but have @@ -157,7 +155,6 @@ def self.included(base) MEDIA_SRC => :source_list, OBJECT_SRC => :source_list, PLUGIN_TYPES => :source_list, - REFLECTED_XSS => :string, REPORT_URI => :source_list, SANDBOX => :source_list, SCRIPT_SRC => :source_list, diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index f2f84315..f5ef1ec7 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -120,7 +120,6 @@ module SecureHeaders end.merge({ block_all_mixed_content: true, upgrade_insecure_requests: true, - reflected_xss: "block", script_src: %w(script-src.com), script_nonce: 123456 }) From fb3da85c2cfef311043dbdf7a979fb0849aa0c17 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 11 Jul 2017 21:11:29 -0700 Subject: [PATCH 381/636] use default csp that is more strict but shouldn't break things terribly --- lib/secure_headers/headers/policy_management.rb | 8 +++++++- .../headers/content_security_policy_spec.rb | 4 ++++ spec/spec_helper.rb | 2 -- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 4512e63b..ba98f4de 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -7,7 +7,13 @@ def self.included(base) MODERN_BROWSERS = %w(Chrome Opera Firefox) DEFAULT_VALUE = "default-src https:".freeze - DEFAULT_CONFIG = { default_src: %w(https:) }.freeze + DEFAULT_CONFIG = { + default_src: %w(https:), + object_src: %w('none'), + script_src: %w(https:), + style_src: %w('self' 'unsafe-inline' https:), + form_action: %w('self') + }.freeze DATA_PROTOCOL = "data:".freeze BLOB_PROTOCOL = "blob:".freeze SELF = "'self'".freeze diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index f2f84315..4b7acfe7 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -24,6 +24,10 @@ module SecureHeaders end describe "#value" do + it "uses a safe but non-breaking default value" do + expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:") + end + it "discards 'none' values if any other source expressions are present" do csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none'))) expect(csp.value).not_to include("'none'") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 62c74ec9..7f086243 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,8 +2,6 @@ require "rubygems" require "rspec" require "rack" -require "pry-nav" - require "coveralls" Coveralls.wear! From 49514ff42dbdd3a9d6d7ecec46a784a572dda596 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 12 Jul 2017 10:43:14 -0300 Subject: [PATCH 382/636] Add "manifest-src" to Chrome/Firefox It is well supported in both now. --- README.md | 1 + lib/secure_headers/headers/policy_management.rb | 6 +++--- .../headers/content_security_policy_spec.rb | 10 +++++----- .../secure_headers/headers/policy_management_spec.rb | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d9a84db5..ba735b1c 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ SecureHeaders::Configuration.default do |config| form_action: %w('self' github.com), frame_ancestors: %w('none'), img_src: %w(mycdn.com data:), + manifest_src: %w('self'), media_src: %w(utoob.com), object_src: %w('self'), plugin_types: %w(application/x-shockwave-flash), diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 92485f9d..18394d34 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -98,15 +98,15 @@ def self.included(base) ].freeze FIREFOX_DIRECTIVES = ( - DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES + DIRECTIVES_3_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES ).freeze FIREFOX_46_DIRECTIVES = ( - DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES + DIRECTIVES_3_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES ).freeze CHROME_DIRECTIVES = ( - DIRECTIVES_2_0 + DIRECTIVES_DRAFT + DIRECTIVES_3_0 + DIRECTIVES_DRAFT ).freeze ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT).uniq.sort diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index f5ef1ec7..ac780bb4 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -127,22 +127,22 @@ module SecureHeaders it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "does not filter any directives for Opera" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do @@ -164,7 +164,7 @@ module SecureHeaders ua = USER_AGENTS[:firefox].dup allow(ua).to receive(:version).and_return(nil) policy = ContentSecurityPolicy.new(complex_opts, ua) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 1d3a2ef3..1855e5a7 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -28,6 +28,7 @@ module SecureHeaders connect_src: %w(wss:), font_src: %w('self' data:), img_src: %w(mycdn.com data:), + manifest_src: %w(manifest.com), media_src: %w(utoob.com), object_src: %w('self'), script_src: %w('self'), From aa1b0304cbabc4a3af8b40edf03a19569db3ce5b Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 12 Jul 2017 10:49:03 -0300 Subject: [PATCH 383/636] Simplify the directives setup by removing the "draft" ones --- .../headers/policy_management.rb | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 18394d34..ae1bcba9 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -63,20 +63,15 @@ def self.included(base) # All the directives currently under consideration for CSP level 3. # https://w3c.github.io/webappsec/specs/CSP2/ + BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content MANIFEST_SRC = :manifest_src + UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests DIRECTIVES_3_0 = [ DIRECTIVES_2_0, - MANIFEST_SRC, - ].flatten.freeze - - # All the directives that are not currently in a formal spec, but have - # been implemented somewhere. - BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content - UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests - DIRECTIVES_DRAFT = [ BLOCK_ALL_MIXED_CONTENT, + MANIFEST_SRC, UPGRADE_INSECURE_REQUESTS - ].freeze + ].flatten.freeze EDGE_DIRECTIVES = DIRECTIVES_1_0 SAFARI_DIRECTIVES = DIRECTIVES_1_0 @@ -98,18 +93,18 @@ def self.included(base) ].freeze FIREFOX_DIRECTIVES = ( - DIRECTIVES_3_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES + DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES ).freeze FIREFOX_46_DIRECTIVES = ( - DIRECTIVES_3_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES + DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES ).freeze CHROME_DIRECTIVES = ( - DIRECTIVES_3_0 + DIRECTIVES_DRAFT + DIRECTIVES_3_0 ).freeze - ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT).uniq.sort + ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort # Think of default-src and report-uri as the beginning and end respectively, # everything else is in between. From cecc908bda43d1c5416093539366e32d40c8bb65 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 11 Jul 2017 21:23:35 -0700 Subject: [PATCH 384/636] Update tests to reflect new defaults/required settings --- .../headers/policy_management.rb | 1 + spec/lib/secure_headers/configuration_spec.rb | 4 +- .../headers/policy_management_spec.rb | 25 +++-- spec/lib/secure_headers_spec.rb | 103 +++++++++++------- 4 files changed, 82 insertions(+), 51 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index ba98f4de..80d8252d 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -210,6 +210,7 @@ def make_header(config, user_agent) def validate_config!(config) return if config.nil? || config.opt_out? raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src) + raise ContentSecurityPolicyConfigError.new(":script is required, falling back to default-src is too dangerous") unless config.directive_value(:script_src) ContentSecurityPolicyConfig.attrs.each do |key| value = config.directive_value(key) next unless value diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 3e161221..48092230 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -71,7 +71,7 @@ module SecureHeaders it "allows you to override an override" do Configuration.override(:override) do |config| - config.csp = { default_src: %w('self')} + config.csp = { default_src: %w('self'), script_src: %w('self')} end Configuration.override(:second_override, :override) do |config| @@ -79,7 +79,7 @@ module SecureHeaders end original_override = Configuration.get(:override) - expect(original_override.csp.to_h).to eq(default_src: %w('self')) + expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self')) override_config = Configuration.get(:second_override) expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org)) end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 1d3a2ef3..6369e687 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -46,7 +46,13 @@ module SecureHeaders it "requires a :default_src value" do expect do - ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self'))) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %w('self'))) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "requires a :script_src value" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end @@ -95,7 +101,7 @@ module SecureHeaders # this is mostly to ensure people don't use the antiquated shorthands common in other configs it "performs light validation on source lists" do expect do - ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval))) + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval), script_src: %w('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end end @@ -104,19 +110,21 @@ module SecureHeaders it "combines the default-src value with the override if the directive was unconfigured" do Configuration.default do |config| config.csp = { - default_src: %w(https:) + default_src: %w(https:), + script_src: %w('self'), } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, style_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) - expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") + expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com") end it "combines directives where the original value is nil and the hash is frozen" do Configuration.default do |config| config.csp = { default_src: %w('self'), + script_src: %w('self'), report_only: false }.freeze end @@ -130,6 +138,7 @@ module SecureHeaders Configuration.default do |config| config.csp = { default_src: %w('self'), + script_src: %w('self'), report_only: false }.freeze end @@ -141,14 +150,13 @@ module SecureHeaders ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) end - - ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value end it "overrides the report_only flag" do Configuration.default do |config| config.csp = { default_src: %w('self'), + script_src: %w('self'), report_only: false } end @@ -161,12 +169,13 @@ module SecureHeaders Configuration.default do |config| config.csp = { default_src: %w(https:), + script_src: %w('self'), block_all_mixed_content: false } end combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) - expect(csp.value).to eq("default-src https:; block-all-mixed-content") + expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'") end it "raises an error if appending to a OPT_OUT policy" do diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 86aca37f..34b9c072 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -31,7 +31,7 @@ module SecureHeaders describe "#header_hash_for" do it "allows you to opt out of individual headers via API" do Configuration.default do |config| - config.csp = { default_src: %w('self')} + config.csp = { default_src: %w('self'), script_src: %w('self')} config.csp_report_only = config.csp end SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY) @@ -63,7 +63,7 @@ module SecureHeaders it "allows you to opt out entirely" do # configure the disabled-by-default headers to ensure they also do not get set Configuration.default do |config| - config.csp = { default_src: ["example.com"] } + config.csp = { default_src: ["example.com"], script_src: %w('self') } config.csp_report_only = config.csp config.hpkp = { report_only: false, @@ -109,6 +109,7 @@ module SecureHeaders Configuration.default do |config| config.csp = { default_src: %w('self'), + script_src: %w('self'), child_src: %w('self') } end @@ -178,38 +179,41 @@ module SecureHeaders Configuration.default do |config| config.csp = { default_src: %w('self'), - frame_src: %w(frame_src.com) + frame_src: %w(frame_src.com), + script_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com)) hash = SecureHeaders.header_hash_for(chrome_request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; child-src frame_src.com child_src.com") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; child-src frame_src.com child_src.com; script-src 'self'") end it "appends frame-src to child-src" do Configuration.default do |config| config.csp = { default_src: %w('self'), - child_src: %w(child_src.com) + child_src: %w(child_src.com), + script_src: %w('self') } end safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari6])) SecureHeaders.append_content_security_policy_directives(safari_request, frame_src: %w(frame_src.com)) hash = SecureHeaders.header_hash_for(safari_request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src child_src.com frame_src.com") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src child_src.com frame_src.com; script-src 'self'") end it "supports named appends" do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end Configuration.named_append(:moar_default_sources) do |request| - { default_src: %w(https:)} + { default_src: %w(https:), style_src: %w('self')} end Configuration.named_append(:how_about_a_script_src_too) do |request| @@ -220,13 +224,14 @@ module SecureHeaders SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too) hash = SecureHeaders.header_hash_for(request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self'") end it "appends a nonce to a missing script-src value" do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end @@ -238,7 +243,8 @@ module SecureHeaders it "appends a hash to a missing script-src value" do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end @@ -250,24 +256,26 @@ module SecureHeaders it "overrides individual directives" do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none')) hash = SecureHeaders.header_hash_for(request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'") end it "overrides non-existant directives" do Configuration.default do |config| config.csp = { - default_src: %w(https:) + default_src: %w(https:), + script_src: %w('self') } end SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'") end it "does not append a nonce when the browser does not support it" do @@ -325,6 +333,7 @@ module SecureHeaders Configuration.default do |config| config.csp = { default_src: %w('self'), + script_src: %w('self'), report_only: true } end @@ -334,7 +343,7 @@ module SecureHeaders hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil - expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'") + expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src 'self'") end it "Raises an error if csp_report_only is used with `report_only: false`" do @@ -342,6 +351,7 @@ module SecureHeaders Configuration.default do |config| config.csp_report_only = { default_src: %w('self'), + script_src: %w('self'), report_only: false } end @@ -352,7 +362,8 @@ module SecureHeaders before(:each) do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } config.csp_report_only = config.csp end @@ -361,82 +372,88 @@ module SecureHeaders it "sets identical values when the configs are the same" do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } config.csp_report_only = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "sets different headers when the configs are different" do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } - config.csp_report_only = config.csp.merge({script_src: %w('self')}) + config.csp_report_only = config.csp.merge({script_src: %w(foo.com)}) end hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' foo.com") end it "allows you to opt-out of enforced CSP" do Configuration.default do |config| config.csp = SecureHeaders::OPT_OUT config.csp_report_only = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to be_nil - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "opts-out of enforced CSP when only csp_report_only is set" do expect(Kernel).to receive(:warn).once Configuration.default do |config| config.csp_report_only = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to be_nil - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "allows you to set csp_report_only before csp" do expect(Kernel).to receive(:warn).once Configuration.default do |config| config.csp_report_only = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')}) end hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "allows appending to the enforced policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "allows appending to the report only policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end @@ -451,13 +468,13 @@ module SecureHeaders SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "allows overriding the report only policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com") end @@ -472,7 +489,8 @@ module SecureHeaders it "updates the enforced header when configured" do Configuration.default do |config| config.csp = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) @@ -486,7 +504,8 @@ module SecureHeaders Configuration.default do |config| config.csp = OPT_OUT config.csp_report_only = { - default_src: %w('self') + default_src: %w('self'), + script_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) @@ -499,17 +518,19 @@ module SecureHeaders it "updates both headers if both are configured" do Configuration.default do |config| config.csp = { - default_src: %w(enforced.com) + default_src: %w(enforced.com), + script_src: %w('self') } config.csp_report_only = { - default_src: %w(reportonly.com) + default_src: %w(reportonly.com), + script_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src enforced.com; script-src enforced.com anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src reportonly.com; script-src reportonly.com anothercdn.com") + expect(hash["Content-Security-Policy"]).to eq("default-src enforced.com; script-src 'self' anothercdn.com") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src reportonly.com; script-src 'self' anothercdn.com") end end From 1b94fe06f7f9a19604f76068de89705ff4f63c56 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 12 Jul 2017 12:21:26 -0700 Subject: [PATCH 385/636] add docs around breaking changes --- CHANGELOG.md | 5 +++++ lib/secure_headers/headers/policy_management.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4479e2e8..5a3e4ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.x + +- `script_src` is required to be set in CSP configs. Falling back to *any* `default-src` can be bad. It's certainly possible for this to not cause a problem but better safe than sorry. +- The default CSP has been changed: `default-src 'self', form-action 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:` This policy is "more secure" and more reasonable. + ## 3.6.5 Update clear-site-data header to use current format specified by the specification. diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 80d8252d..dd2c5730 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -210,7 +210,7 @@ def make_header(config, user_agent) def validate_config!(config) return if config.nil? || config.opt_out? raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src) - raise ContentSecurityPolicyConfigError.new(":script is required, falling back to default-src is too dangerous") unless config.directive_value(:script_src) + raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous") unless config.directive_value(:script_src) ContentSecurityPolicyConfig.attrs.each do |key| value = config.directive_value(key) next unless value From 3e3f99c016f816ae4889d2acd33f930813eb6f27 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 09:15:29 -1000 Subject: [PATCH 386/636] fix guard for recent rubies" --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index b4565c97..111b8c8b 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,6 @@ end group :guard do gem "growl" - gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] + gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24] gem "rb-fsevent" end From 17a1677993aab1d057c8a8da61ddd5bf3c69e8e0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 09:16:18 -1000 Subject: [PATCH 387/636] set a more lenient img-src --- lib/secure_headers/headers/policy_management.rb | 1 + spec/lib/secure_headers/headers/content_security_policy_spec.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index dd2c5730..ad41c215 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -9,6 +9,7 @@ def self.included(base) DEFAULT_VALUE = "default-src https:".freeze DEFAULT_CONFIG = { default_src: %w(https:), + img_src: %w(https: data: 'self'), object_src: %w('none'), script_src: %w(https:), style_src: %w('self' 'unsafe-inline' https:), diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 4b7acfe7..3d3af897 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -25,7 +25,7 @@ module SecureHeaders describe "#value" do it "uses a safe but non-breaking default value" do - expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:") + expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:") end it "discards 'none' values if any other source expressions are present" do From 517493e83ba525a7e5fe160adf926b1c838dacb4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 09:29:14 -1000 Subject: [PATCH 388/636] Add note about upcoming 4.x line in readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba735b1c..3b88ecbf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) +**master represents the unreleased 4.x line**. See the [upgrading to 4.x doc](upgrading-to-4-0.md) for instructions on how to upgrade. Bug fixes should go in the 3.x branch for now. -**The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. +**The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. -**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be maintained**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. +**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be not be maintained once 4.x is released. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) From f9ec5bf781b9d0803da96959164ae50380166e39 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 09:37:00 -1000 Subject: [PATCH 389/636] Create upgrading-to-4-0.md --- upgrading-to-4-0.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 upgrading-to-4-0.md diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md new file mode 100644 index 00000000..4c25a740 --- /dev/null +++ b/upgrading-to-4-0.md @@ -0,0 +1,33 @@ +### Breaking Changes + +## Default Content Security Policy + +The default CSP has changed to be more universal without sacrificing too much security. + +* Flash/Java disabled by default +* `img-src` allows data: images and favicons (among others) +* `style-src` allows inline CSS by default (most find it impossible/impractical to remove inline content today) +* `form-action` (not governed by `default-src`, practically treated as `*`) is set to `'self'` + +Previously, the default CSP was: + +`Content-Security-Policy: default-src 'self'` + +The new default policy is: + +`default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:` + +## All cookies default to secure/httponly + +By default, all cookies will be marked as `secure` and `httponly`. To opt-out, supply `OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs: + +```ruby +config.cookies = { + secure: OPT_OUT, + httponly: OPT_OUT, +} +``` + +## Supported ruby versions + +We've dropped support for ruby versions <= 2.2. Sorry. From 5f7d8050419d69399f0dbe069f646c9ca8bdeab2 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 09:53:54 -1000 Subject: [PATCH 390/636] make deprecations around setting CSP out of order turn into exceptions --- Gemfile | 2 +- lib/secure_headers/configuration.rb | 9 ++-- spec/lib/secure_headers_spec.rb | 65 ++++++++++++----------------- spec/spec_helper.rb | 1 - 4 files changed, 30 insertions(+), 47 deletions(-) diff --git a/Gemfile b/Gemfile index b4565c97..111b8c8b 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,6 @@ end group :guard do gem "growl" - gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] + gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24] gem "rb-fsevent" end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index bdadc6c0..2ba5d62c 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -215,10 +215,8 @@ def csp=(new_csp) @csp = new_csp.dup else if new_csp[:report_only] - # Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out - Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" - @csp = OPT_OUT - self.csp_report_only = new_csp + # invalid configuration implies that CSPRO should be set, CSP should not - so opt out + raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" else @csp = ContentSecurityPolicyConfig.new(new_csp) end @@ -247,8 +245,7 @@ def csp_report_only=(new_csp) end if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT - Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp=" - @csp = OPT_OUT + raise ArgumentError, "#{Kernel.caller.first}: `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp=" end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 86aca37f..28d57b2c 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -319,22 +319,15 @@ module SecureHeaders expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'") end - it "supports the deprecated `report_only: true` format" do - expect(Kernel).to receive(:warn).once - - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - report_only: true - } - end - - expect(Configuration.get.csp).to eq(OPT_OUT) - expect(Configuration.get.csp_report_only).to be_a(ContentSecurityPolicyReportOnlyConfig) - - hash = SecureHeaders.header_hash_for(request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil - expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'") + it "does not support the deprecated `report_only: true` format" do + expect { + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + report_only: true + } + end + }.to raise_error(ArgumentError) end it "Raises an error if csp_report_only is used with `report_only: false`" do @@ -399,31 +392,25 @@ module SecureHeaders expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") end - it "opts-out of enforced CSP when only csp_report_only is set" do - expect(Kernel).to receive(:warn).once - Configuration.default do |config| - config.csp_report_only = { - default_src: %w('self') - } - end - - hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to be_nil - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") + it "raises an error when only csp_report_only is set" do + expect { + Configuration.default do |config| + config.csp_report_only = { + default_src: %w('self') + } + end + }.to raise_error(ArgumentError) end - it "allows you to set csp_report_only before csp" do - expect(Kernel).to receive(:warn).once - Configuration.default do |config| - config.csp_report_only = { - default_src: %w('self') - } - config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')}) - end - - hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'") + it "does not allow you to set csp_report_only before csp" do + expect { + Configuration.default do |config| + config.csp_report_only = { + default_src: %w('self') + } + config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')}) + end + }.to raise_error(ArgumentError) end it "allows appending to the enforced policy" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 62c74ec9..6ff6d60c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,7 +2,6 @@ require "rubygems" require "rspec" require "rack" -require "pry-nav" require "coveralls" Coveralls.wear! From 4815c263b820f61045789e9b991b4882f8ba415f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 09:57:22 -1000 Subject: [PATCH 391/636] more notes around CSP deprecated changes --- README.md | 1 - lib/secure_headers/configuration.rb | 4 ---- spec/lib/secure_headers_spec.rb | 21 --------------------- upgrading-to-4-0.md | 9 +++++++-- 4 files changed, 7 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 3b88ecbf..e90889fc 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,6 @@ SecureHeaders::Configuration.default do |config| ] config.csp = { # "meta" values. these will shaped the header, but the values are not included in the header. - # report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only] preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 2ba5d62c..6de9e4a0 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -243,10 +243,6 @@ def csp_report_only=(new_csp) end end end - - if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT - raise ArgumentError, "#{Kernel.caller.first}: `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp=" - end end protected diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 2477e139..0ac25816 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -406,27 +406,6 @@ module SecureHeaders expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end - it "raises an error when only csp_report_only is set" do - expect { - Configuration.default do |config| - config.csp_report_only = { - default_src: %w('self') - } - end - }.to raise_error(ArgumentError) - end - - it "does not allow you to set csp_report_only before csp" do - expect { - Configuration.default do |config| - config.csp_report_only = { - default_src: %w('self') - } - config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')}) - end - }.to raise_error(ArgumentError) - end - it "allows appending to the enforced policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index 4c25a740..f8e7b7fc 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -2,7 +2,7 @@ ## Default Content Security Policy -The default CSP has changed to be more universal without sacrificing too much security. +The default CSP has changed to be more universal without sacrificing too much security. * Flash/Java disabled by default * `img-src` allows data: images and favicons (among others) @@ -17,13 +17,18 @@ The new default policy is: `default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:` +## CSP configuration + +* Setting `report_only: true` in a CSP config will raise an error. Instead, set `csp_report_only`. + + ## All cookies default to secure/httponly By default, all cookies will be marked as `secure` and `httponly`. To opt-out, supply `OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs: ```ruby config.cookies = { - secure: OPT_OUT, + secure: OPT_OUT, httponly: OPT_OUT, } ``` From a9985724042160b8dae86e1987d696e58429c072 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 11:10:57 -1000 Subject: [PATCH 392/636] raise errors when child/frame-src values don't match --- .../headers/content_security_policy.rb | 10 ++------- .../headers/content_security_policy_spec.rb | 18 ++++------------ spec/lib/secure_headers_spec.rb | 21 ++++--------------- upgrading-to-4-0.md | 1 + 4 files changed, 11 insertions(+), 39 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 7be6a859..aad6bf14 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -55,18 +55,12 @@ def value private - # frame-src is deprecated, child-src is being implemented. They are - # very similar and in most cases, the same value can be used for both. def normalize_child_frame_src if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src - Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.") + raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers." end - if supported_directives.include?(:child_src) - @config.child_src || @config.frame_src - else - @config.frame_src || @config.child_src - end + @config.frame_src || @config.child_src end # Private: converts the config object into a string representing a policy. diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 62532826..69d7da08 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -96,20 +96,10 @@ module SecureHeaders ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value end - it "emits a warning when child-src and frame-src are supplied but are not equal" do - expect(Kernel).to receive(:warn).with(/both :child_src and :frame_src supplied and do not match./) - ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value - end - - it "will still set inconsistent child/frame-src values to be less surprising" do - expect(Kernel).to receive(:warn).at_least(:once) - firefox = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox]).value - firefox_transitional = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox46]).value - expect(firefox).not_to eq(firefox_transitional) - expect(firefox).to match(/frame-src/) - expect(firefox).not_to match(/child-src/) - expect(firefox_transitional).to match(/child-src/) - expect(firefox_transitional).not_to match(/frame-src/) + it "raises an error when child-src and frame-src are supplied but are not equal" do + expect { + ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value + }.to raise_error(ArgumentError) end it "supports strict-dynamic" do diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 0ac25816..75a40b7b 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -175,7 +175,7 @@ module SecureHeaders expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end - it "appends child-src to frame-src" do + it "child-src and frame-src must match" do Configuration.default do |config| config.csp = { default_src: %w('self'), @@ -185,23 +185,10 @@ module SecureHeaders end SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com)) - hash = SecureHeaders.header_hash_for(chrome_request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; child-src frame_src.com child_src.com; script-src 'self'") - end - - it "appends frame-src to child-src" do - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - child_src: %w(child_src.com), - script_src: %w('self') - } - end - safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari6])) - SecureHeaders.append_content_security_policy_directives(safari_request, frame_src: %w(frame_src.com)) - hash = SecureHeaders.header_hash_for(safari_request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src child_src.com frame_src.com; script-src 'self'") + expect { + SecureHeaders.header_hash_for(chrome_request) + }.to raise_error(ArgumentError) end it "supports named appends" do diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index f8e7b7fc..579a3679 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -20,6 +20,7 @@ The new default policy is: ## CSP configuration * Setting `report_only: true` in a CSP config will raise an error. Instead, set `csp_report_only`. +* Setting `frame_src` and `child_src` when values don't match will raise an error. Just use `frame_src`. ## All cookies default to secure/httponly From f6f6aba433e6a3624472fef931c00aa9955a2d62 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 11:18:11 -1000 Subject: [PATCH 393/636] don't support the #secure_cookies method --- lib/secure_headers/configuration.rb | 3 +-- spec/lib/secure_headers/configuration_spec.rb | 10 +++++----- spec/lib/secure_headers/middleware_spec.rb | 10 +++------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 6de9e4a0..4e674bef 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -206,8 +206,7 @@ def validate_config! end def secure_cookies=(secure_cookies) - Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead." - @cookies = (@cookies || {}).merge(secure: secure_cookies) + raise ArgumentError, "#{Kernel.caller.first}: `#secure_cookies=` is no longer supported. Please use `#cookies=` to configure secure cookies instead." end def csp=(new_csp) diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 48092230..ed54bb2b 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -85,11 +85,11 @@ module SecureHeaders end it "deprecates the secure_cookies configuration" do - expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/) - - Configuration.default do |config| - config.secure_cookies = true - end + expect { + Configuration.default do |config| + config.secure_cookies = true + end + }.to raise_error(ArgumentError) end end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 82b759b8..7ed04f9d 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -55,12 +55,10 @@ module SecureHeaders expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org") end - context "secure_cookies" do + context "cookies" do context "cookies should be flagged" do it "flags cookies as secure" do - capture_warning do - Configuration.default { |config| config.secure_cookies = true } - end + Configuration.default { |config| config.cookies = { secure: true } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar; secure") @@ -69,9 +67,7 @@ module SecureHeaders context "cookies should not be flagged" do it "does not flags cookies as secure" do - capture_warning do - Configuration.default { |config| config.secure_cookies = false } - end + Configuration.default { |config| config.cookies = nil } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") From 0534134ec8433fb1df23a8a1338d3c3ef4219c94 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 18 Jul 2017 11:19:36 -1000 Subject: [PATCH 394/636] [skip ci] add docs about removed #secure_cookies API --- upgrading-to-4-0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index 579a3679..36b559e9 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -34,6 +34,10 @@ config.cookies = { } ``` +## config.secure_cookies removed + +Use `config.cookies` instead. + ## Supported ruby versions We've dropped support for ruby versions <= 2.2. Sorry. From dfe67310c3ac51b5c59c7e656cfb25a5441a9bd4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 21 Jul 2017 11:42:31 -1000 Subject: [PATCH 395/636] Update README.md --- README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/README.md b/README.md index 3b88ecbf..05e65f61 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. -**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be not be maintained once 4.x is released. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. +**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be not be maintained once 4.x is released**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) @@ -133,20 +133,6 @@ X-Permitted-Cross-Domain-Policies: none X-Xss-Protection: 1; mode=block ``` -### Default CSP - -By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. - -```ruby -Configuration.default do |config| - config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. - config.csp_report_only = { - default_src: %w('self') - } -end -``` - - ## Similar libraries * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers) From 067936443865d87b21cda1986ec6b4d8cba59fe0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 21 Jul 2017 11:53:42 -1000 Subject: [PATCH 396/636] post install message + more docs --- .../headers/policy_management.rb | 1 - secure_headers.gemspec | 7 +++++ upgrading-to-4-0.md | 30 +++++++++++-------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 7500b7c2..3373addb 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -6,7 +6,6 @@ def self.included(base) end MODERN_BROWSERS = %w(Chrome Opera Firefox) - DEFAULT_VALUE = "default-src https:".freeze DEFAULT_CONFIG = { default_src: %w(https:), img_src: %w(https: data: 'self'), diff --git a/secure_headers.gemspec b/secure_headers.gemspec index a7e064b5..c6a1d912 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -17,4 +17,11 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.add_development_dependency "rake" gem.add_dependency "useragent", ">= 0.15.0" + + # TODO: delete this after 4.1 is cut or a number of 4.0.x releases have occurred + gem.post_install_message = <<~POST_INSTALL + :wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of + security!). It's highly likely you will need to update your configuration to + avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md + POST_INSTALL end diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index 36b559e9..4530e7c7 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -1,5 +1,23 @@ ### Breaking Changes +The most likely change to break your app is the new cookie defaults. This is the first place to check. If you're using the default CSP, your policy will change but your app should not break. + +## All cookies default to secure/httponly + +By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs: + +```ruby +# specific opt outs +config.cookies = { + secure: OPT_OUT, + httponly: OPT_OUT, + samesite: OPT_OUT, +} + +# nuclear option, just make things work again +config.cookies = OPT_OUT +``` + ## Default Content Security Policy The default CSP has changed to be more universal without sacrificing too much security. @@ -22,18 +40,6 @@ The new default policy is: * Setting `report_only: true` in a CSP config will raise an error. Instead, set `csp_report_only`. * Setting `frame_src` and `child_src` when values don't match will raise an error. Just use `frame_src`. - -## All cookies default to secure/httponly - -By default, all cookies will be marked as `secure` and `httponly`. To opt-out, supply `OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs: - -```ruby -config.cookies = { - secure: OPT_OUT, - httponly: OPT_OUT, -} -``` - ## config.secure_cookies removed Use `config.cookies` instead. From 6379fa00ce3ccaa9cefa562a19b886c08d40c7e7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 21 Jul 2017 11:56:23 -1000 Subject: [PATCH 397/636] better post install message --- secure_headers.gemspec | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index c6a1d912..fb88bd97 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -20,8 +20,10 @@ Gem::Specification.new do |gem| # TODO: delete this after 4.1 is cut or a number of 4.0.x releases have occurred gem.post_install_message = <<~POST_INSTALL - :wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of - security!). It's highly likely you will need to update your configuration to - avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md + + ********** + :wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md + ********** + POST_INSTALL end From 547741b66b50015eb9e582e5fa152b55e4812d89 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 21 Jul 2017 12:08:45 -1000 Subject: [PATCH 398/636] Default samesite: true to mean SameSite=Lax instead of Strict --- lib/secure_headers/headers/cookie.rb | 4 ++++ spec/lib/secure_headers/headers/cookie_spec.rb | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 2c683d1c..60aa067a 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -100,6 +100,10 @@ def flag_samesite_strict? def flag_samesite_enforcement?(mode) return unless config[:samesite] + if config[:samesite].is_a?(TrueClass) && mode == :lax + return true + end + case config[:samesite][mode] when Hash conditionally_flag?(config[:samesite][mode]) diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index bdb84b1b..19dd6737 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -104,6 +104,12 @@ module SecureHeaders cookie = Cookie.new(raw_cookie, samesite: { lax: true }) expect(cookie.to_s).to eq(raw_cookie) end + + it "samesite: true sets all cookies to samesite=lax" do + raw_cookie = "_session=thisisatest" + cookie = Cookie.new(raw_cookie, samesite: true) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") + end end end From 1e7d777538b9c84530aa32913bb26d83e2083250 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 21 Jul 2017 12:17:30 -1000 Subject: [PATCH 399/636] ugh, ruby versions --- secure_headers.gemspec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index fb88bd97..a9f5306d 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -19,11 +19,11 @@ Gem::Specification.new do |gem| gem.add_dependency "useragent", ">= 0.15.0" # TODO: delete this after 4.1 is cut or a number of 4.0.x releases have occurred - gem.post_install_message = <<~POST_INSTALL + gem.post_install_message = <<-POST_INSTALL - ********** - :wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md - ********** +********** +:wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md +********** POST_INSTALL end From 19850dd3fc266719ec9afac213f055fa04604492 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Fri, 14 Jul 2017 14:11:15 -0700 Subject: [PATCH 400/636] Adds secure and httponly to cookies by default --- lib/secure_headers/headers/cookie.rb | 7 ++++ .../lib/secure_headers/headers/cookie_spec.rb | 41 +++++++++++-------- spec/lib/secure_headers/middleware_spec.rb | 10 ++--- spec/spec_helper.rb | 12 ------ 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 2c683d1c..5efe1321 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -16,6 +16,11 @@ def validate_config!(config) def initialize(cookie, config) @raw_cookie = cookie + unless config == SecureHeaders::OPT_OUT + config ||= {} + config[:secure] = true if config[:secure].nil? + config[:httponly] = true if config[:httponly].nil? + end @config = config @attributes = { httponly: nil, @@ -57,6 +62,7 @@ def already_flagged?(attribute) end def flag_cookie?(attribute) + return false if config == SecureHeaders::OPT_OUT case config[attribute] when TrueClass true @@ -86,6 +92,7 @@ def samesite_cookie end def flag_samesite? + return false if config == SecureHeaders::OPT_OUT flag_samesite_lax? || flag_samesite_strict? end diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index bdb84b1b..22fc4482 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -5,37 +5,42 @@ module SecureHeaders describe Cookie do let(:raw_cookie) { "_session=thisisatest" } - it "does not tamper with cookies when unconfigured" do - cookie = Cookie.new(raw_cookie, {}) + it "does not tamper with cookies when using OPT_OUT is used" do + cookie = Cookie.new(raw_cookie, OPT_OUT) expect(cookie.to_s).to eq(raw_cookie) end + it "applies httponly and secure by default" do + cookie = Cookie.new(raw_cookie, nil) + expect(cookie.to_s).to eq("_session=thisisatest; secure; HttpOnly") + end + it "preserves existing attributes" do - cookie = Cookie.new("_session=thisisatest; secure", secure: true) + cookie = Cookie.new("_session=thisisatest; secure", {secure: true, httponly: false}) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "prevents duplicate flagging of attributes" do - cookie = Cookie.new("_session=thisisatest; secure", secure: true) + cookie = Cookie.new("_session=thisisatest; secure", {secure: true, httponly: false}) expect(cookie.to_s.scan(/secure/i).count).to eq(1) end context "Secure cookies" do context "when configured with a boolean" do it "flags cookies as Secure" do - cookie = Cookie.new(raw_cookie, secure: true) + cookie = Cookie.new(raw_cookie, secure: true, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest; secure") end end context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do - cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}) + cookie = Cookie.new(raw_cookie, {secure: { only: ["_session"]}, httponly: false}) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "does not flag cookies as Secure when excluded" do - cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }) + cookie = Cookie.new(raw_cookie, {secure: { except: ["_session"] }, httponly: false}) expect(cookie.to_s).to eq("_session=thisisatest") end end @@ -44,19 +49,19 @@ module SecureHeaders context "HttpOnly cookies" do context "when configured with a boolean" do it "flags cookies as HttpOnly" do - cookie = Cookie.new(raw_cookie, httponly: true) + cookie = Cookie.new(raw_cookie, httponly: true, secure: false) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end end context "when configured with a Hash" do it "flags cookies as HttpOnly when whitelisted" do - cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}) + cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}, secure: false) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end it "does not flag cookies as HttpOnly when excluded" do - cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }) + cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }, secure: false) expect(cookie.to_s).to eq("_session=thisisatest") end end @@ -64,44 +69,44 @@ module SecureHeaders context "SameSite cookies" do it "flags SameSite=Lax" do - cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }) + cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }, secure: false, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end it "flags SameSite=Lax when configured with a boolean" do - cookie = Cookie.new(raw_cookie, samesite: { lax: true}) + cookie = Cookie.new(raw_cookie, samesite: { lax: true}, secure: false, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end it "does not flag cookies as SameSite=Lax when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }) + cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }, secure: false, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest") end it "flags SameSite=Strict" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }) + cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }, secure: false, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "does not flag cookies as SameSite=Strict when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } }) + cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] }}, secure: false, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest") end it "flags SameSite=Strict when configured with a boolean" do - cookie = Cookie.new(raw_cookie, samesite: { strict: true}) + cookie = Cookie.new(raw_cookie, {samesite: { strict: true}, secure: false, httponly: false}) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "flags properly when both lax and strict are configured" do raw_cookie = "_session=thisisatest" - cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }) + cookie = Cookie.new(raw_cookie, {samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }, secure: false, httponly: false}) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "ignores configuration if the cookie is already flagged" do raw_cookie = "_session=thisisatest; SameSite=Strict" - cookie = Cookie.new(raw_cookie, samesite: { lax: true }) + cookie = Cookie.new(raw_cookie, {samesite: { lax: true }, secure: false, httponly: false}) expect(cookie.to_s).to eq(raw_cookie) end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 7ed04f9d..3e81ab66 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -58,7 +58,7 @@ module SecureHeaders context "cookies" do context "cookies should be flagged" do it "flags cookies as secure" do - Configuration.default { |config| config.cookies = { secure: true } } + Configuration.default { |config| config.cookies = {secure: true, httponly: false} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar; secure") @@ -67,7 +67,7 @@ module SecureHeaders context "cookies should not be flagged" do it "does not flags cookies as secure" do - Configuration.default { |config| config.cookies = nil } + Configuration.default { |config| config.cookies = {secure: false, httponly: false} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") @@ -87,7 +87,7 @@ module SecureHeaders it "flags cookies with a combination of SameSite configurations" do cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] }) - Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } } } } + Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: false, secure: false} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env @@ -96,7 +96,7 @@ module SecureHeaders end it "disables secure cookies for non-https requests" do - Configuration.default { |config| config.cookies = { secure: true } } + Configuration.default { |config| config.cookies = { secure: true, httponly: false} } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env @@ -104,7 +104,7 @@ module SecureHeaders end it "sets the secure cookie flag correctly on interleaved http/https requests" do - Configuration.default { |config| config.cookies = { secure: true } } + Configuration.default { |config| config.cookies = { secure: true, httponly: false } } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7f086243..1f4d3227 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,15 +48,3 @@ def clear_configurations def reset_config SecureHeaders::Configuration.clear_configurations end - -def capture_warning - begin - old_stderr = $stderr - $stderr = StringIO.new - yield - result = $stderr.string - ensure - $stderr = old_stderr - end - result -end From 8f7757e82f10dbfba6c1c1361b095a0568f1e376 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Fri, 14 Jul 2017 15:13:25 -0700 Subject: [PATCH 401/636] Style fixes --- spec/lib/secure_headers/headers/cookie_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index 22fc4482..f4ae495d 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -16,12 +16,12 @@ module SecureHeaders end it "preserves existing attributes" do - cookie = Cookie.new("_session=thisisatest; secure", {secure: true, httponly: false}) + cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "prevents duplicate flagging of attributes" do - cookie = Cookie.new("_session=thisisatest; secure", {secure: true, httponly: false}) + cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: false) expect(cookie.to_s.scan(/secure/i).count).to eq(1) end @@ -35,12 +35,12 @@ module SecureHeaders context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do - cookie = Cookie.new(raw_cookie, {secure: { only: ["_session"]}, httponly: false}) + cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "does not flag cookies as Secure when excluded" do - cookie = Cookie.new(raw_cookie, {secure: { except: ["_session"] }, httponly: false}) + cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest") end end @@ -100,13 +100,13 @@ module SecureHeaders it "flags properly when both lax and strict are configured" do raw_cookie = "_session=thisisatest" - cookie = Cookie.new(raw_cookie, {samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }, secure: false, httponly: false}) + cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }, secure: false, httponly: false) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "ignores configuration if the cookie is already flagged" do raw_cookie = "_session=thisisatest; SameSite=Strict" - cookie = Cookie.new(raw_cookie, {samesite: { lax: true }, secure: false, httponly: false}) + cookie = Cookie.new(raw_cookie, samesite: { lax: true }, secure: false, httponly: false) expect(cookie.to_s).to eq(raw_cookie) end end From 810e2c1e99f76de67d56c451f0a6218084c0cb72 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Fri, 14 Jul 2017 15:19:48 -0700 Subject: [PATCH 402/636] Updates documentation detailing the new cookie defaults --- docs/cookies.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/cookies.md b/docs/cookies.md index 1aff0125..c53855bf 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -4,6 +4,17 @@ SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf. __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. +#### Defaults + +By default, all cookies will get both `Secure` and `HttpOnly`. + +```ruby +config.cookies = { + secure: true, # defaults to true but will be a no op on non-HTTPS requests + httponly: true, # defaults to true +} +``` + #### Boolean-based configuration Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. From 0ae4c00624cb29d1e1621961396b88b55b4e23f6 Mon Sep 17 00:00:00 2001 From: anglinb Date: Fri, 21 Jul 2017 19:26:31 -0700 Subject: [PATCH 403/636] Forces explict OPT_OUT --- lib/secure_headers/headers/cookie.rb | 9 ++++- lib/secure_headers/middleware.rb | 2 +- lib/secure_headers/utils/cookies_config.rb | 19 +++++---- .../lib/secure_headers/headers/cookie_spec.rb | 40 +++++++++++-------- spec/lib/secure_headers/middleware_spec.rb | 10 ++--- 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 5efe1321..d9a0e515 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -2,6 +2,7 @@ require "cgi" require "secure_headers/utils/cookies_config" + module SecureHeaders class CookiesConfigError < StandardError; end class Cookie @@ -14,12 +15,16 @@ def validate_config!(config) attr_reader :raw_cookie, :config + COOKIE_DEFAULTS = { + httponly: true, + secure: true, + }.freeze + def initialize(cookie, config) @raw_cookie = cookie unless config == SecureHeaders::OPT_OUT config ||= {} - config[:secure] = true if config[:secure].nil? - config[:httponly] = true if config[:httponly].nil? + config = COOKIE_DEFAULTS.merge(config) end @config = config @attributes = { diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 52cc1319..7515e653 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -39,7 +39,7 @@ def flag_cookies!(headers, config) # disable Secure cookies for non-https requests def override_secure(env, config = {}) if scheme(env) != "https" - config[:secure] = false + config[:secure] = OPT_OUT end config diff --git a/lib/secure_headers/utils/cookies_config.rb b/lib/secure_headers/utils/cookies_config.rb index 707feadc..10c0bb0c 100644 --- a/lib/secure_headers/utils/cookies_config.rb +++ b/lib/secure_headers/utils/cookies_config.rb @@ -12,9 +12,9 @@ def validate! return if config.nil? || config == SecureHeaders::OPT_OUT validate_config! - validate_secure_config! if config[:secure] - validate_httponly_config! if config[:httponly] - validate_samesite_config! if config[:samesite] + validate_secure_config! unless config[:secure].nil? + validate_httponly_config! unless config[:httponly].nil? + validate_samesite_config! unless config[:samesite].nil? end private @@ -24,12 +24,12 @@ def validate_config! end def validate_secure_config! - validate_hash_or_boolean!(:secure) + validate_hash_or_true_or_opt_out!(:secure) validate_exclusive_use_of_hash_constraints!(config[:secure], :secure) end def validate_httponly_config! - validate_hash_or_boolean!(:httponly) + validate_hash_or_true_or_opt_out!(:httponly) validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly) end @@ -62,8 +62,8 @@ def validate_samesite_hash_config! end end - def validate_hash_or_boolean!(attribute) - if !(is_hash?(config[attribute]) || is_boolean?(config[attribute])) + def validate_hash_or_true_or_opt_out!(attribute) + if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute])) raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") end end @@ -71,7 +71,6 @@ def validate_hash_or_boolean!(attribute) # validate exclusive use of only or except but not both at the same time def validate_exclusive_use_of_hash_constraints!(conf, attribute) return unless is_hash?(conf) - if conf.key?(:only) && conf.key?(:except) raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") end @@ -88,8 +87,8 @@ def is_hash?(obj) obj && obj.is_a?(Hash) end - def is_boolean?(obj) - obj && (obj.is_a?(TrueClass) || obj.is_a?(FalseClass)) + def is_true_or_opt_out?(obj) + obj && (obj.is_a?(TrueClass) || obj == OPT_OUT) end end end diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index f4ae495d..8f98a3b9 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -16,31 +16,31 @@ module SecureHeaders end it "preserves existing attributes" do - cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: false) + cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "prevents duplicate flagging of attributes" do - cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: false) + cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: OPT_OUT) expect(cookie.to_s.scan(/secure/i).count).to eq(1) end context "Secure cookies" do context "when configured with a boolean" do it "flags cookies as Secure" do - cookie = Cookie.new(raw_cookie, secure: true, httponly: false) + cookie = Cookie.new(raw_cookie, secure: true, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end end context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do - cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}, httponly: false) + cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "does not flag cookies as Secure when excluded" do - cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }, httponly: false) + cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end end @@ -49,19 +49,19 @@ module SecureHeaders context "HttpOnly cookies" do context "when configured with a boolean" do it "flags cookies as HttpOnly" do - cookie = Cookie.new(raw_cookie, httponly: true, secure: false) + cookie = Cookie.new(raw_cookie, httponly: true, secure: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end end context "when configured with a Hash" do it "flags cookies as HttpOnly when whitelisted" do - cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}, secure: false) + cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}, secure: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end it "does not flag cookies as HttpOnly when excluded" do - cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }, secure: false) + cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }, secure: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end end @@ -69,44 +69,44 @@ module SecureHeaders context "SameSite cookies" do it "flags SameSite=Lax" do - cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }, secure: false, httponly: false) + cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end it "flags SameSite=Lax when configured with a boolean" do - cookie = Cookie.new(raw_cookie, samesite: { lax: true}, secure: false, httponly: false) + cookie = Cookie.new(raw_cookie, samesite: { lax: true}, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end it "does not flag cookies as SameSite=Lax when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }, secure: false, httponly: false) + cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end it "flags SameSite=Strict" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }, secure: false, httponly: false) + cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "does not flag cookies as SameSite=Strict when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] }}, secure: false, httponly: false) + cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] }}, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end it "flags SameSite=Strict when configured with a boolean" do - cookie = Cookie.new(raw_cookie, {samesite: { strict: true}, secure: false, httponly: false}) + cookie = Cookie.new(raw_cookie, {samesite: { strict: true}, secure: OPT_OUT, httponly: OPT_OUT}) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "flags properly when both lax and strict are configured" do raw_cookie = "_session=thisisatest" - cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }, secure: false, httponly: false) + cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "ignores configuration if the cookie is already flagged" do raw_cookie = "_session=thisisatest; SameSite=Strict" - cookie = Cookie.new(raw_cookie, samesite: { lax: true }, secure: false, httponly: false) + cookie = Cookie.new(raw_cookie, samesite: { lax: true }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq(raw_cookie) end end @@ -119,12 +119,18 @@ module SecureHeaders end.to raise_error(CookiesConfigError) end - it "raises an exception when configured without a boolean/Hash" do + it "raises an exception when configured without a boolean(true or OPT_OUT)/Hash" do expect do Cookie.validate_config!(secure: "true") end.to raise_error(CookiesConfigError) end + it "raises an exception when configured with false" do + expect do + Cookie.validate_config!(secure: false) + end.to raise_error(CookiesConfigError) + end + it "raises an exception when both only and except filters are provided" do expect do Cookie.validate_config!(secure: { only: [], except: [] }) diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 3e81ab66..261ffaf7 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -58,7 +58,7 @@ module SecureHeaders context "cookies" do context "cookies should be flagged" do it "flags cookies as secure" do - Configuration.default { |config| config.cookies = {secure: true, httponly: false} } + Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar; secure") @@ -67,7 +67,7 @@ module SecureHeaders context "cookies should not be flagged" do it "does not flags cookies as secure" do - Configuration.default { |config| config.cookies = {secure: false, httponly: false} } + Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") @@ -87,7 +87,7 @@ module SecureHeaders it "flags cookies with a combination of SameSite configurations" do cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] }) - Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: false, secure: false} } + Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env @@ -96,7 +96,7 @@ module SecureHeaders end it "disables secure cookies for non-https requests" do - Configuration.default { |config| config.cookies = { secure: true, httponly: false} } + Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT} } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env @@ -104,7 +104,7 @@ module SecureHeaders end it "sets the secure cookie flag correctly on interleaved http/https requests" do - Configuration.default { |config| config.cookies = { secure: true, httponly: false } } + Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT } } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env From d0026969fa471ddb02083e9303217419b83b585e Mon Sep 17 00:00:00 2001 From: anglinb Date: Fri, 21 Jul 2017 20:33:53 -0700 Subject: [PATCH 404/636] Adds SameSite=Lax default --- docs/cookies.md | 7 +++++-- lib/secure_headers/headers/cookie.rb | 7 ++++--- lib/secure_headers/utils/cookies_config.rb | 1 + spec/lib/secure_headers/headers/cookie_spec.rb | 18 +++++++++--------- spec/lib/secure_headers/middleware_spec.rb | 12 ++++++------ 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/docs/cookies.md b/docs/cookies.md index c53855bf..8b742cb9 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -12,17 +12,20 @@ By default, all cookies will get both `Secure` and `HttpOnly`. config.cookies = { secure: true, # defaults to true but will be a no op on non-HTTPS requests httponly: true, # defaults to true + samesite: { # defaults to set `SameSite=Lax` + lax: true + } } ``` #### Boolean-based configuration -Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. +Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. *Note: As of 4.0, you must use OPT_OUT rather than false to opt out of the defaults.* ```ruby config.cookies = { secure: true, # mark all cookies as Secure - httponly: false, # do not mark any cookies as HttpOnly + httponly: OPT_OUT, # do not mark any cookies as HttpOnly } ``` diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index d9a0e515..824d3daa 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -18,11 +18,12 @@ def validate_config!(config) COOKIE_DEFAULTS = { httponly: true, secure: true, + samesite: { lax: true }, }.freeze def initialize(cookie, config) @raw_cookie = cookie - unless config == SecureHeaders::OPT_OUT + unless config == OPT_OUT config ||= {} config = COOKIE_DEFAULTS.merge(config) end @@ -67,7 +68,7 @@ def already_flagged?(attribute) end def flag_cookie?(attribute) - return false if config == SecureHeaders::OPT_OUT + return false if config == OPT_OUT case config[attribute] when TrueClass true @@ -97,7 +98,7 @@ def samesite_cookie end def flag_samesite? - return false if config == SecureHeaders::OPT_OUT + return false if config == OPT_OUT or config[:samesite] == OPT_OUT flag_samesite_lax? || flag_samesite_strict? end diff --git a/lib/secure_headers/utils/cookies_config.rb b/lib/secure_headers/utils/cookies_config.rb index 10c0bb0c..468e66a7 100644 --- a/lib/secure_headers/utils/cookies_config.rb +++ b/lib/secure_headers/utils/cookies_config.rb @@ -34,6 +34,7 @@ def validate_httponly_config! end def validate_samesite_config! + return if config[:samesite] == OPT_OUT raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite]) validate_samesite_boolean_config! diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index 8f98a3b9..bb857f3c 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -10,13 +10,13 @@ module SecureHeaders expect(cookie.to_s).to eq(raw_cookie) end - it "applies httponly and secure by default" do + it "applies httponly, secure, and samesite by default" do cookie = Cookie.new(raw_cookie, nil) - expect(cookie.to_s).to eq("_session=thisisatest; secure; HttpOnly") + expect(cookie.to_s).to eq("_session=thisisatest; secure; HttpOnly; SameSite=Lax") end it "preserves existing attributes" do - cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: OPT_OUT) + cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end @@ -28,19 +28,19 @@ module SecureHeaders context "Secure cookies" do context "when configured with a boolean" do it "flags cookies as Secure" do - cookie = Cookie.new(raw_cookie, secure: true, httponly: OPT_OUT) + cookie = Cookie.new(raw_cookie, secure: true, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end end context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do - cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}, httponly: OPT_OUT) + cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "does not flag cookies as Secure when excluded" do - cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }, httponly: OPT_OUT) + cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end end @@ -49,19 +49,19 @@ module SecureHeaders context "HttpOnly cookies" do context "when configured with a boolean" do it "flags cookies as HttpOnly" do - cookie = Cookie.new(raw_cookie, httponly: true, secure: OPT_OUT) + cookie = Cookie.new(raw_cookie, httponly: true, secure: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end end context "when configured with a Hash" do it "flags cookies as HttpOnly when whitelisted" do - cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}, secure: OPT_OUT) + cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}, secure: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end it "does not flag cookies as HttpOnly when excluded" do - cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }, secure: OPT_OUT) + cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }, secure: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 261ffaf7..697d1e84 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -58,7 +58,7 @@ module SecureHeaders context "cookies" do context "cookies should be flagged" do it "flags cookies as secure" do - Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT} } + Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar; secure") @@ -67,7 +67,7 @@ module SecureHeaders context "cookies should not be flagged" do it "does not flags cookies as secure" do - Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT} } + Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT, samesite: OPT_OUT} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") @@ -77,11 +77,11 @@ module SecureHeaders context "cookies" do it "flags cookies from configuration" do - Configuration.default { |config| config.cookies = { secure: true, httponly: true } } + Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env - expect(env["Set-Cookie"]).to eq("foo=bar; secure; HttpOnly") + expect(env["Set-Cookie"]).to eq("foo=bar; secure; HttpOnly; SameSite=Lax") end it "flags cookies with a combination of SameSite configurations" do @@ -96,7 +96,7 @@ module SecureHeaders end it "disables secure cookies for non-https requests" do - Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT} } + Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env @@ -104,7 +104,7 @@ module SecureHeaders end it "sets the secure cookie flag correctly on interleaved http/https requests" do - Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT } } + Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env From 592aa90c4c6450ad27a477c7096839462ecc34c5 Mon Sep 17 00:00:00 2001 From: Didde Brockman Date: Mon, 24 Jul 2017 20:01:39 +0200 Subject: [PATCH 405/636] Typo So `shaped` becomes `shape`. --- spec/lib/secure_headers/headers/policy_management_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 894f8b17..319ffb9b 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -17,7 +17,7 @@ module SecureHeaders it "accepts all keys" do # (pulled from README) config = { - # "meta" values. these will shaped the header, but the values are not included in the header. + # "meta" values. these will shape the header, but the values are not included in the header. report_only: true, # default: false preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. From 552afc08287dbef0a4af2659d52133b8e4756968 Mon Sep 17 00:00:00 2001 From: Didde Brockman Date: Mon, 24 Jul 2017 20:03:53 +0200 Subject: [PATCH 406/636] Typo 2 So `shaped` becomes `shape`. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f715a8db..56c883f0 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ SecureHeaders::Configuration.default do |config| "executionContexts" ] config.csp = { - # "meta" values. these will shaped the header, but the values are not included in the header. + # "meta" values. these will shape the header, but the values are not included in the header. preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives From 17ae48c0c03b5208cd0846609bcbb60b90d5b8fa Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 24 Jul 2017 09:43:59 -1000 Subject: [PATCH 407/636] Update docs to reflect new defaults --- docs/cookies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookies.md b/docs/cookies.md index 8b742cb9..d136953c 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -6,7 +6,7 @@ __Note__: Regardless of the configuration specified, Secure cookies are only ena #### Defaults -By default, all cookies will get both `Secure` and `HttpOnly`. +By default, all cookies will get both `Secure`, `HttpOnly`, and `SameSite=Lax`. ```ruby config.cookies = { From d3f883ec3e678ba3883459dee2657e096d221aa2 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 24 Jul 2017 09:44:51 -1000 Subject: [PATCH 408/636] Use `||` instead of `or` --- lib/secure_headers/headers/cookie.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 824d3daa..b80db3a1 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -98,7 +98,7 @@ def samesite_cookie end def flag_samesite? - return false if config == OPT_OUT or config[:samesite] == OPT_OUT + return false if config == OPT_OUT || config[:samesite] == OPT_OUT flag_samesite_lax? || flag_samesite_strict? end From 4813308dd5fcb6eba612a8924d53a7b49b535da1 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 24 Jul 2017 10:09:15 -1000 Subject: [PATCH 409/636] fix broken test due to changed defaults --- spec/lib/secure_headers/headers/cookie_spec.rb | 2 +- upgrading-to-4-0.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index b61298d7..db7eb0c1 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -112,7 +112,7 @@ module SecureHeaders it "samesite: true sets all cookies to samesite=lax" do raw_cookie = "_session=thisisatest" - cookie = Cookie.new(raw_cookie, samesite: true) + cookie = Cookie.new(raw_cookie, samesite: true, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end end diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index 4530e7c7..f88a4f55 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -2,9 +2,9 @@ The most likely change to break your app is the new cookie defaults. This is the first place to check. If you're using the default CSP, your policy will change but your app should not break. -## All cookies default to secure/httponly +## All cookies default to secure/httponly/SameSite=Lax -By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs: +By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs. Setting these values to `false` will raise an error. ```ruby # specific opt outs From a410f15ba54ec9bcd55af43894630398c9b88d09 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 24 Jul 2017 10:26:51 -1000 Subject: [PATCH 410/636] bump and docs 4.0.0.alpha01 --- CHANGELOG.md | 3 +-- secure_headers.gemspec | 2 +- upgrading-to-4-0.md | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3e4ebf..392a19c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ ## 4.x -- `script_src` is required to be set in CSP configs. Falling back to *any* `default-src` can be bad. It's certainly possible for this to not cause a problem but better safe than sorry. -- The default CSP has been changed: `default-src 'self', form-action 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:` This policy is "more secure" and more reasonable. +- See the [upgrading to 4.0](upgrading-to-4.0.md) guide. Lots of breaking changes. ## 3.6.5 diff --git a/secure_headers.gemspec b/secure_headers.gemspec index a9f5306d..84600377 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "4.x.x" + gem.version = "4.0.0.alpha01" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index f88a4f55..988c235f 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -1,6 +1,6 @@ ### Breaking Changes -The most likely change to break your app is the new cookie defaults. This is the first place to check. If you're using the default CSP, your policy will change but your app should not break. +The most likely change to break your app is the new cookie defaults. This is the first place to check. If you're using the default CSP, your policy will change but your app should not break. This should not break brand new projects using secure_headers either. ## All cookies default to secure/httponly/SameSite=Lax From 713da82bdc1bf013d6acad54fbe133f7481083ce Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 24 Jul 2017 11:52:16 -1000 Subject: [PATCH 411/636] allow opting out of script-src requirement --- lib/secure_headers/headers/content_security_policy.rb | 8 +++++--- lib/secure_headers/headers/policy_management.rb | 8 ++++++-- .../headers/content_security_policy_spec.rb | 5 +++++ spec/lib/secure_headers/headers/policy_management_spec.rb | 6 ++++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index aad6bf14..d635c8dc 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -101,9 +101,11 @@ def build_directive(directive) else @config.directive_value(directive) end - return unless source_list && source_list.any? - normalized_source_list = minify_source_list(directive, source_list) - [symbol_to_hyphen_case(directive), normalized_source_list].join(" ") + + if source_list != OPT_OUT && source_list&.any? + normalized_source_list = minify_source_list(directive, source_list) + [symbol_to_hyphen_case(directive), normalized_source_list].join(" ") + end end # If a directive contains *, all other values are omitted. diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 3373addb..ddead4c3 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -202,7 +202,10 @@ def make_header(config, user_agent) def validate_config!(config) return if config.nil? || config.opt_out? raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src) - raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous") unless config.directive_value(:script_src) + if config.directive_value(:script_src).nil? + raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use OPT_OUT to override") + end + ContentSecurityPolicyConfig.attrs.each do |key| value = config.directive_value(key) next unless value @@ -342,12 +345,13 @@ def ensure_valid_directive!(directive) end def ensure_array_of_strings!(directive, source_expression) - unless source_expression.is_a?(Array) && source_expression.compact.all? { |v| v.is_a?(String) } + if (!source_expression.is_a?(Array) || !source_expression.compact.all? { |v| v.is_a?(String) }) && source_expression != OPT_OUT raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings") end end def ensure_valid_sources!(directive, source_expression) + return if source_expression == OPT_OUT source_expression.each do |expression| if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression) raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.") diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 69d7da08..73de51a4 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -51,6 +51,11 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org") end + it "does not build directives with a value of OPT_OUT (and bypasses directive requirements)" do + csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), script_src: OPT_OUT) + expect(csp.value).to eq("default-src example.org") + end + it "does not remove schemes from report-uri values" do csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org)) expect(csp.value).to eq("default-src https:; report-uri https://example.org") diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 319ffb9b..2d36045b 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -57,6 +57,12 @@ module SecureHeaders end.to raise_error(ContentSecurityPolicyConfigError) end + it "accepts OPT_OUT as a script-src value" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: OPT_OUT)) + end.to_not raise_error + end + it "requires :report_only to be a truthy value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve"))) From 884cda24de328f28f93c50b642629af641a040b4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 24 Jul 2017 13:04:33 -1000 Subject: [PATCH 412/636] can't use &. yet --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index d635c8dc..2a132eb8 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -102,7 +102,7 @@ def build_directive(directive) @config.directive_value(directive) end - if source_list != OPT_OUT && source_list&.any? + if source_list != OPT_OUT && source_list && source_list.any? normalized_source_list = minify_source_list(directive, source_list) [symbol_to_hyphen_case(directive), normalized_source_list].join(" ") end From bd3ee4576e357ff8e6f0a6d19ddf32a32d2ac787 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 24 Jul 2017 13:22:56 -1000 Subject: [PATCH 413/636] make note about overriding script-src requirement clearer --- lib/secure_headers/headers/policy_management.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index ddead4c3..5baf81b6 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -203,7 +203,7 @@ def validate_config!(config) return if config.nil? || config.opt_out? raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src) if config.directive_value(:script_src).nil? - raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use OPT_OUT to override") + raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override") end ContentSecurityPolicyConfig.attrs.each do |key| From 501270c96b455560ac629da8ca00433c64a7dec5 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 25 Jul 2017 08:21:04 -1000 Subject: [PATCH 414/636] bumpt to 4.0.0.alpha02 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 84600377..eb03d0da 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "4.0.0.alpha01" + gem.version = "4.0.0.alpha02" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 1dbc59053187928338162e2d87fdc8112145dffb Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 25 Jul 2017 11:45:59 -1000 Subject: [PATCH 415/636] don't try to modify an opt-out config for cookies --- lib/secure_headers.rb | 2 +- lib/secure_headers/middleware.rb | 2 +- spec/lib/secure_headers/middleware_spec.rb | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 5f0349e5..70a4cd27 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -24,7 +24,7 @@ module SecureHeaders class NoOpHeaderConfig include Singleton - def boom(arg = nil) + def boom(*args) raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead." end diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 7515e653..910c386c 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -38,7 +38,7 @@ def flag_cookies!(headers, config) # disable Secure cookies for non-https requests def override_secure(env, config = {}) - if scheme(env) != "https" + if scheme(env) != "https" && config != OPT_OUT config[:secure] = OPT_OUT end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 697d1e84..8d827254 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -65,6 +65,13 @@ module SecureHeaders end end + it "allows opting out of cookie protection with OPT_OUT alone" do + Configuration.default { |config| config.cookies = OPT_OUT} + request = Rack::Request.new({}) + _, env = cookie_middleware.call request.env + expect(env["Set-Cookie"]).to eq("foo=bar") + end + context "cookies should not be flagged" do it "does not flags cookies as secure" do Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT, samesite: OPT_OUT} } From 5d046a924b34085a122ea8ab0c0f133058a74176 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 25 Jul 2017 12:19:17 -1000 Subject: [PATCH 416/636] add long comment about ensuring this specific test remains non-https --- spec/lib/secure_headers/middleware_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 8d827254..cc49cb5a 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -67,6 +67,10 @@ module SecureHeaders it "allows opting out of cookie protection with OPT_OUT alone" do Configuration.default { |config| config.cookies = OPT_OUT} + + # do NOT make this request https. non-https requests modify a config, + # causing an exception when operating on OPT_OUT. This ensures we don't + # try to modify the config. request = Rack::Request.new({}) _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") From a2d363e9077f5f8a85abf99b5bfcaaac800fa00e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 25 Jul 2017 13:58:26 -1000 Subject: [PATCH 417/636] bump to alpha03 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index eb03d0da..0cca594d 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "4.0.0.alpha02" + gem.version = "4.0.0.alpha03" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 1eb5493b560d8e929c3d3b464a1517aaecd25543 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 25 Jul 2017 14:05:46 -1000 Subject: [PATCH 418/636] add note about script-src being required and how to opt out --- upgrading-to-4-0.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index 988c235f..d0753054 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -18,6 +18,12 @@ config.cookies = { config.cookies = OPT_OUT ``` +## script_src must be set + +Not setting a `script_src` value means your policy falls back to whatever `default_src` (also required) is set to. This can be very dangerous and indicates the policy is too loose. + +However, sometimes you really don't need a `script-src` e.g. API responses (`default-src 'none'`) so you can set `script_src: SecureHeaders::OPT_OUT` to work around this. + ## Default Content Security Policy The default CSP has changed to be more universal without sacrificing too much security. From a1daa24de59523ae13730518d484c3a075b1e74f Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Sat, 26 Aug 2017 07:53:52 +1000 Subject: [PATCH 419/636] Add support for `Expect-CT` HTTP header (#322) * Update README with example usage for `Expect-CT` * Add some tests for the expected API * Add classes and loading to main entrypoints * Add `ExpectCt` class Adds the `ExpectCt` class and it's associated validation of the header syntax * Use full name of specification instead * Use consistent double quotes * Update README example to use correct naming for Expect-CT * Set defaults for Expect-CT via helper * Add missing comma in README example * Add frozen string literal --- README.md | 6 ++ lib/secure_headers.rb | 2 + lib/secure_headers/configuration.rb | 4 +- .../expect_certificate_transparency.rb | 70 +++++++++++++++++++ .../headers/expect_certificate_spec.rb | 42 +++++++++++ spec/spec_helper.rb | 1 + 6 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 lib/secure_headers/headers/expect_certificate_transparency.rb create mode 100644 spec/lib/secure_headers/headers/expect_certificate_spec.rb diff --git a/README.md b/README.md index 56c883f0..371377b4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ The gem will automatically apply several headers that are related to security. - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/) - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) +- Expect-CT - Only use certificates that are present in the certificate transparency logs. [Expect-CT draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/). - Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/). It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so). @@ -77,6 +78,11 @@ SecureHeaders::Configuration.default do |config| "storage", "executionContexts" ] + config.expect_certificate_transparency = { + enforce: false, + max_age: 1.day.to_i, + report_uri: "https://report-uri.io/example-ct" + } config.csp = { # "meta" values. these will shape the header, but the values are not included in the header. preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 70a4cd27..a0b7a39a 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -12,6 +12,7 @@ require "secure_headers/headers/x_permitted_cross_domain_policies" require "secure_headers/headers/referrer_policy" require "secure_headers/headers/clear_site_data" +require "secure_headers/headers/expect_certificate_transparency" require "secure_headers/middleware" require "secure_headers/railtie" require "secure_headers/view_helper" @@ -52,6 +53,7 @@ def opt_out? CSP = ContentSecurityPolicy ALL_HEADER_CLASSES = [ + ExpectCertificateTransparency, ClearSiteData, ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig, diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 4e674bef..493adfda 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -117,7 +117,7 @@ def deep_copy_if_hash(value) attr_writer :hsts, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, - :referrer_policy, :clear_site_data + :referrer_policy, :clear_site_data, :expect_certificate_transparency attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host @@ -169,6 +169,7 @@ def dup copy.x_download_options = @x_download_options copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies copy.clear_site_data = @clear_site_data + copy.expect_certificate_transparency = @expect_certificate_transparency copy.referrer_policy = @referrer_policy copy.hpkp = @hpkp copy.hpkp_report_host = @hpkp_report_host @@ -201,6 +202,7 @@ def validate_config! XDownloadOptions.validate_config!(@x_download_options) XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies) ClearSiteData.validate_config!(@clear_site_data) + ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency) PublicKeyPins.validate_config!(@hpkp) Cookie.validate_config!(@cookies) end diff --git a/lib/secure_headers/headers/expect_certificate_transparency.rb b/lib/secure_headers/headers/expect_certificate_transparency.rb new file mode 100644 index 00000000..8086cfd6 --- /dev/null +++ b/lib/secure_headers/headers/expect_certificate_transparency.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true +module SecureHeaders + class ExpectCertificateTransparencyConfigError < StandardError; end + + class ExpectCertificateTransparency + HEADER_NAME = "Expect-CT".freeze + CONFIG_KEY = :expect_certificate_transparency + INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze + INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze + REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze + INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze + + class << self + # Public: Generate a Expect-CT header. + # + # Returns nil if not configured, returns header name and value if + # configured. + def make_header(config) + return if config.nil? + + header = new(config) + [HEADER_NAME, header.value] + end + + def validate_config!(config) + return if config.nil? || config == OPT_OUT + raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash + + unless [true, false, nil].include?(config[:enforce]) + raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR) + end + + if !config[:max_age] + raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR) + elsif config[:max_age].to_s !~ /\A\d+\z/ + raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR) + end + end + end + + def initialize(config) + @enforced = config.fetch(:enforce, nil) + @max_age = config.fetch(:max_age, nil) + @report_uri = config.fetch(:report_uri, nil) + end + + def value + header_value = [ + enforced_directive, + max_age_directive, + report_uri_directive + ].compact.join("; ").strip + end + + def enforced_directive + # Unfortunately `if @enforced` isn't enough here in case someone + # passes in a random string so let's be specific with it to prevent + # accidental enforcement. + "enforce" if @enforced == true + end + + def max_age_directive + "max-age=#{@max_age}" if @max_age + end + + def report_uri_directive + "report-uri=\"#{@report_uri}\"" if @report_uri + end + end +end diff --git a/spec/lib/secure_headers/headers/expect_certificate_spec.rb b/spec/lib/secure_headers/headers/expect_certificate_spec.rb new file mode 100644 index 00000000..86735d79 --- /dev/null +++ b/spec/lib/secure_headers/headers/expect_certificate_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +require "spec_helper" + +module SecureHeaders + describe ExpectCertificateTransparency do + specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce; max-age=1234") } + specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") } + specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") } + specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"") } + specify do + config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" } + header_value = "enforce; max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"" + expect(ExpectCertificateTransparency.new(config).value).to eq(header_value) + end + + context "with an invalid configuration" do + it "raises an exception when configuration isn't a hash" do + expect do + ExpectCertificateTransparency.validate_config!(%w(a)) + end.to raise_error(ExpectCertificateTransparencyConfigError) + end + + it "raises an exception when max-age is not provided" do + expect do + ExpectCertificateTransparency.validate_config!(foo: "bar") + end.to raise_error(ExpectCertificateTransparencyConfigError) + end + + it "raises an exception with an invalid max-age" do + expect do + ExpectCertificateTransparency.validate_config!(max_age: "abc123") + end.to raise_error(ExpectCertificateTransparencyConfigError) + end + + it "raises an exception with an invalid enforce value" do + expect do + ExpectCertificateTransparency.validate_config!(enforce: "brokenstring") + end.to raise_error(ExpectCertificateTransparencyConfigError) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1f4d3227..0c00eb92 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -33,6 +33,7 @@ def expect_default_values(hash) expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE) expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil + expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil end module SecureHeaders From c1e90f55629802a66ed6db68c9bbc57e32b63eb8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 25 Aug 2017 14:59:16 -0700 Subject: [PATCH 420/636] fix uninit'd warning --- lib/secure_headers/configuration.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 493adfda..92032de1 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -144,6 +144,7 @@ def initialize(&block) @x_frame_options = nil @x_permitted_cross_domain_policies = nil @x_xss_protection = nil + @expect_certificate_transparency = nil self.hpkp = OPT_OUT self.referrer_policy = OPT_OUT From 65eef3c36f1634f42820852c9becb10a02172393 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 25 Aug 2017 17:19:16 -0700 Subject: [PATCH 421/636] fix unused var warning --- lib/secure_headers/headers/expect_certificate_transparency.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/expect_certificate_transparency.rb b/lib/secure_headers/headers/expect_certificate_transparency.rb index 8086cfd6..bda4d937 100644 --- a/lib/secure_headers/headers/expect_certificate_transparency.rb +++ b/lib/secure_headers/headers/expect_certificate_transparency.rb @@ -45,7 +45,7 @@ def initialize(config) end def value - header_value = [ + [ enforced_directive, max_age_directive, report_uri_directive From 44c7d278ea375faf959746525ae80a6ffde3478d Mon Sep 17 00:00:00 2001 From: Koen Rouwhorst Date: Mon, 28 Aug 2017 11:19:29 +0200 Subject: [PATCH 422/636] Fixed URL to 'Upgrading to 4.0' document. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 392a19c9..dde785ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## 4.x -- See the [upgrading to 4.0](upgrading-to-4.0.md) guide. Lots of breaking changes. +- See the [upgrading to 4.0](upgrading-to-4-0.md) guide. Lots of breaking changes. ## 3.6.5 From 2bb810ad45382cb1e1da2c8c4d995ee21a9241e0 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 5 Sep 2017 15:25:52 -0600 Subject: [PATCH 423/636] Distinguish between source lists directives and non-source list directives (#354) * Add more specific type support for non "source list" directives Prior behavior treated anything that was an "array of stuff" as a "source list". But, source lists are very specific and don't really have meaning for some directives. For example, the sandbox directive take an array of values, but they are very specific. They aren't URLs, don't support wildcard values, etc. So, This commit extract those directives from being treated like source lists and adds native support for the associated directive. * Make overall naming more consistent and adds some polish * Remove unnecessary nonce logic and special case OPT_OUT for source lists --- .../headers/content_security_policy.rb | 45 +++++- .../headers/policy_management.rb | 137 ++++++++++++------ .../headers/content_security_policy_spec.rb | 35 +++-- .../headers/policy_management_spec.rb | 30 ++++ 4 files changed, 186 insertions(+), 61 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 2a132eb8..42751817 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -75,20 +75,55 @@ def build_value case DIRECTIVE_VALUE_TYPES[directive_name] when :boolean symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) - when :string - [symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ") - else - build_directive(directive_name) + when :sandbox_list + build_sandbox_list_directive(directive_name) + when :media_type_list + build_media_type_list_directive(directive_name) + when :source_list + build_source_list_directive(directive_name) end end.compact.join("; ") end + def build_sandbox_list_directive(directive) + return unless sandbox_list = @config.directive_value(directive) + max_strict_policy = case sandbox_list + when Array + sandbox_list.empty? + when true + true + else + false + end + + # A maximally strict sandbox policy is just the `sandbox` directive, + # whith no configuraiton values. + if max_strict_policy + symbol_to_hyphen_case(directive) + elsif sandbox_list && sandbox_list.any? + [ + symbol_to_hyphen_case(directive), + sandbox_list.uniq + ].join(" ") + end + end + + def build_media_type_list_directive(directive) + return unless media_type_list = @config.directive_value(directive) + if media_type_list && media_type_list.any? + [ + symbol_to_hyphen_case(directive), + media_type_list.uniq + ].join(" ") + end + end + # Private: builds a string that represents one directive in a minified form. # # directive_name - a symbol representing the various ALL_DIRECTIVES # # Returns a string representing a directive. - def build_directive(directive) + def build_source_list_directive(directive) source_list = case directive when :child_src if supported_directives.include?(:child_src) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 5baf81b6..47771924 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -116,18 +116,6 @@ def self.included(base) # everything else is in between. BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] - # These are directives that do not inherit the default-src value. This is - # useful when calling #combine_policies. - NON_FETCH_SOURCES = [ - BASE_URI, - FORM_ACTION, - FRAME_ANCESTORS, - PLUGIN_TYPES, - REPORT_URI - ] - - FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - VARIATIONS = { "Chrome" => CHROME_DIRECTIVES, "Opera" => CHROME_DIRECTIVES, @@ -155,14 +143,30 @@ def self.included(base) MANIFEST_SRC => :source_list, MEDIA_SRC => :source_list, OBJECT_SRC => :source_list, - PLUGIN_TYPES => :source_list, + PLUGIN_TYPES => :media_type_list, REPORT_URI => :source_list, - SANDBOX => :source_list, + SANDBOX => :sandbox_list, SCRIPT_SRC => :source_list, STYLE_SRC => :source_list, UPGRADE_INSECURE_REQUESTS => :boolean }.freeze + # These are directives that don't have use a source list, and hence do not + # inherit the default-src value. + NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type| + type != :source_list + end.keys.freeze + + # These are directives that take a source list, but that do not inherit + # the default-src value. + NON_FETCH_SOURCES = [ + BASE_URI, + FORM_ACTION, + FRAME_ANCESTORS, + REPORT_URI + ] + + FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) HTTP_SCHEME_REGEX = %r{\Ahttps?://} @@ -264,7 +268,7 @@ def ua_to_variation(user_agent) # when each hash contains a value for a given key. def merge_policy_additions(original, additions) original.merge(additions) do |directive, lhs, rhs| - if source_list?(directive) + if list_directive?(directive) (lhs.to_a + rhs.to_a).compact.uniq else rhs @@ -272,20 +276,27 @@ def merge_policy_additions(original, additions) end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives. end + # Returns True if a directive expects a list of values and False otherwise. + def list_directive?(directive) + source_list?(directive) || + sandbox_list?(directive) || + media_type_list?(directive) + end + # For each directive in additions that does not exist in the original config, # copy the default-src value to the original config. This modifies the original hash. def populate_fetch_source_with_default!(original, additions) # in case we would be appending to an empty directive, fill it with the default-src value additions.each_key do |directive| - if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions)) - if nonce_added?(original, additions) - inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym - unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive) - original[inferred_directive] = default_for(directive, original) - end - else - original[directive] = default_for(directive, original) - end + directive = if directive.to_s.end_with?("_nonce") + directive.to_s.gsub(/_nonce/, "_src").to_sym + else + directive + end + # Don't set a default if directive has an existing value + next if original[directive] + if FETCH_SOURCES.include?(directive) + original[directive] = default_for(directive, original) end end end @@ -296,45 +307,77 @@ def default_for(directive, original) original[DEFAULT_SRC] end - def nonce_added?(original, additions) - [:script_nonce, :style_nonce].each do |nonce| - if additions[nonce] && !original[nonce] - return true - end - end - end - def source_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :source_list end + def sandbox_list?(directive) + DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list + end + + def media_type_list?(directive) + DIRECTIVE_VALUE_TYPES[directive] == :media_type_list + end + # Private: Validates that the configuration has a valid type, or that it is a valid # source expression. - def validate_directive!(directive, source_expression) + def validate_directive!(directive, value) + ensure_valid_directive!(directive) case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive] when :boolean - unless boolean?(source_expression) - raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value") - end - when :string - unless source_expression.is_a?(String) - raise ContentSecurityPolicyConfigError.new("#{directive} Must be a string. Found #{config.class}: #{config} value") + unless boolean?(value) + raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value") end + when :sandbox_list + validate_sandbox_expression!(directive, value) + when :media_type_list + validate_media_type_expression!(directive, value) + when :source_list + validate_source_expression!(directive, value) else - validate_source_expression!(directive, source_expression) + raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") + end + end + + # Private: validates that a sandbox token expression: + # 1. is an array of strings or optionally `true` (to enable maximal sandboxing) + # 2. For arrays, each element is of the form allow-* + def validate_sandbox_expression!(directive, sandbox_token_expression) + # We support sandbox: true to indicate a maximally secure sandbox. + return if boolean?(sandbox_token_expression) && sandbox_token_expression == true + ensure_array_of_strings!(directive, sandbox_token_expression) + valid = sandbox_token_expression.compact.all? do |v| + v.is_a?(String) && v.start_with?("allow-") + end + if !valid + raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)") + end + end + + # Private: validates that a media type expression: + # 1. is an array of strings + # 2. each element is of the form type/subtype + def validate_media_type_expression!(directive, media_type_expression) + ensure_array_of_strings!(directive, media_type_expression) + valid = media_type_expression.compact.all? do |v| + # All media types are of the form: "/" . + v =~ /\A.+\/.+\z/ + end + if !valid + raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)") end end # Private: validates that a source expression: - # 1. has a valid name - # 2. is an array of strings - # 3. does not contain any depreated, now invalid values (inline, eval, self, none) + # 1. is an array of strings + # 2. does not contain any deprecated, now invalid values (inline, eval, self, none) # # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_source_expression!(directive, source_expression) - ensure_valid_directive!(directive) - ensure_array_of_strings!(directive, source_expression) + if source_expression != OPT_OUT + ensure_array_of_strings!(directive, source_expression) + end ensure_valid_sources!(directive, source_expression) end @@ -344,8 +387,8 @@ def ensure_valid_directive!(directive) end end - def ensure_array_of_strings!(directive, source_expression) - if (!source_expression.is_a?(Array) || !source_expression.compact.all? { |v| v.is_a?(String) }) && source_expression != OPT_OUT + def ensure_array_of_strings!(directive, value) + if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) }) raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings") end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 73de51a4..44e0c5fa 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -96,6 +96,21 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org") end + it "creates maximally strict sandbox policy when passed no sandbox token values" do + csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: []) + expect(csp.value).to eq("default-src example.org; sandbox") + end + + it "creates maximally strict sandbox policy when passed true" do + csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: true) + expect(csp.value).to eq("default-src example.org; sandbox") + end + + it "creates sandbox policy when passed valid sandbox token values" do + csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: %w(allow-forms allow-scripts)) + expect(csp.value).to eq("default-src example.org; sandbox allow-forms allow-scripts") + end + it "does not emit a warning when using frame-src" do expect(Kernel).to_not receive(:warn) ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value @@ -120,50 +135,52 @@ module SecureHeaders block_all_mixed_content: true, upgrade_insecure_requests: true, script_src: %w(script-src.com), - script_nonce: 123456 + script_nonce: 123456, + sandbox: %w(allow-forms), + plugin_types: %w(application/pdf) }) end it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "does not filter any directives for Opera" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge]) - expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) - expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com") end it "falls back to standard Firefox defaults when the useragent version is not present" do ua = USER_AGENTS[:firefox].dup allow(ua).to receive(:version).and_return(nil) policy = ContentSecurityPolicy.new(complex_opts, ua) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 2d36045b..9d7af025 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -111,6 +111,36 @@ module SecureHeaders ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval), script_src: %w('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end + + it "rejects anything not of the form allow-* as a sandbox value" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["steve"]))) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "accepts anything of the form allow-* as a sandbox value " do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["allow-foo"]))) + end.to_not raise_error + end + + it "accepts true as a sandbox policy" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: true))) + end.to_not raise_error + end + + it "rejects anything not of the form type/subtype as a plugin-type value" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["steve"]))) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "accepts anything of the form type/subtype as a plugin-type value " do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"]))) + end.to_not raise_error + end end describe "#combine_policies" do From 8e6f963f54511f399ef911c611baccaed6bc2c52 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 5 Sep 2017 11:31:01 -1000 Subject: [PATCH 424/636] bump for sandbox fix --- README.md | 1 + secure_headers.gemspec | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 371377b4..7528b9c7 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ SecureHeaders::Configuration.default do |config| manifest_src: %w('self'), media_src: %w(utoob.com), object_src: %w('self'), + sandbox: true, # true and [] will set a maximally restrictive setting plugin_types: %w(application/x-shockwave-flash), script_src: %w('self'), style_src: %w('unsafe-inline'), diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 0cca594d..a02a1b7d 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "4.0.0.alpha03" + gem.version = "4.0.0.alpha04" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From dfd70f4f43672eab17e03398731dca7fcec74e2b Mon Sep 17 00:00:00 2001 From: Srikanth Samudrala Date: Sat, 16 Sep 2017 14:04:06 +0530 Subject: [PATCH 425/636] Changes to the documentation to make it more explicit that provided configuration should only be used as a reference --- README.md | 2 ++ docs/per_action_configuration.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/README.md b/README.md index 7528b9c7..eb77d40d 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ If you do not supply a `default` configuration, exceptions will be raised. If yo All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. +**Word of caution:** The following is not a default configuration per se. It serves as a reference to all the available options. You should read more about these headers and determine what is appropriate for your requirements. + ```ruby SecureHeaders::Configuration.default do |config| config.cookies = { diff --git a/docs/per_action_configuration.md b/docs/per_action_configuration.md index 95799313..1e98ee9e 100644 --- a/docs/per_action_configuration.md +++ b/docs/per_action_configuration.md @@ -103,3 +103,32 @@ Content-Security-Policy: ... console.log("won't execute, not whitelisted") ``` + +## Clearing browser cache + +You can clear the browser cache after the logout request by using the following. + +``` ruby +class ApplicationController < ActionController::Base + # Configuration override to send the Clear-Site-Data header. + SecureHeaders::Configuration.override(:clear_browser_cache) do |config| + config.clear_site_data = [ + SecureHeaders::ClearSiteData::ALL + ] + end + + + # Clears the browser's cache for browsers supporting the Clear-Site-Data + # header. + # + # Returns nothing. + def clear_browser_cache + SecureHeaders.use_secure_headers_override(request, :clear_browser_cache) + end +end + +class SessionsController < ApplicationController + after_filter :clear_browser_cache, only: :destroy +end + +``` \ No newline at end of file From 02808fc4e5831d5e5b5fa96ca8de275d39a7df79 Mon Sep 17 00:00:00 2001 From: Srikanth Samudrala Date: Sat, 16 Sep 2017 14:04:20 +0530 Subject: [PATCH 426/636] Corrected a spelling mistake --- lib/secure_headers/headers/clear_site_data.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index 3c7cfc9f..34f09d0d 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -8,8 +8,8 @@ class ClearSiteData CACHE = "cache".freeze COOKIES = "cookies".freeze STORAGE = "storage".freeze - EXECTION_CONTEXTS = "executionContexts".freeze - ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_CONTEXTS] + EXECUTION_CONTEXTS = "executionContexts".freeze + ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS] CONFIG_KEY = :clear_site_data From 7102240d8344c1885ae61ab6b8f8d55f5699a75d Mon Sep 17 00:00:00 2001 From: Srikanth Samudrala Date: Mon, 18 Sep 2017 12:12:07 +0530 Subject: [PATCH 427/636] addressed the comments from the review --- README.md | 2 +- docs/per_action_configuration.md | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eb77d40d..5d97020e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ If you do not supply a `default` configuration, exceptions will be raised. If yo All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. -**Word of caution:** The following is not a default configuration per se. It serves as a reference to all the available options. You should read more about these headers and determine what is appropriate for your requirements. +**Word of caution:** The following is not a default configuration per se. It serves as a sample implementation of the configuration. You should read more about these headers and determine what is appropriate for your requirements. ```ruby SecureHeaders::Configuration.default do |config| diff --git a/docs/per_action_configuration.md b/docs/per_action_configuration.md index 1e98ee9e..b8750d31 100644 --- a/docs/per_action_configuration.md +++ b/docs/per_action_configuration.md @@ -113,7 +113,7 @@ class ApplicationController < ActionController::Base # Configuration override to send the Clear-Site-Data header. SecureHeaders::Configuration.override(:clear_browser_cache) do |config| config.clear_site_data = [ - SecureHeaders::ClearSiteData::ALL + SecureHeaders::ClearSiteData::ALL_TYPES ] end @@ -128,7 +128,6 @@ class ApplicationController < ActionController::Base end class SessionsController < ApplicationController - after_filter :clear_browser_cache, only: :destroy + after_action :clear_browser_cache, only: :destroy end - ``` \ No newline at end of file From 7bafd11186decd2da9b69e9bbaeaf41c4f20670b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 18 Sep 2017 13:47:36 -1000 Subject: [PATCH 428/636] bump to 4.0.0 --- CHANGELOG.md | 12 ++++++++++++ secure_headers.gemspec | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dde785ab..36604f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ - See the [upgrading to 4.0](upgrading-to-4-0.md) guide. Lots of breaking changes. +## 3.7.1 + +Fix support for the sandbox attribute of CSP. `true` and `[]` represent the maximally restricted policy (`sandbox;`) and validate other values. + +## 3.7.0 + +Adds support for the `Expect-CT` header (@jacobbednarz: https://github.com/twitter/secureheaders/pull/322) + +## 3.6.7 + +Actually set manifest-src when configured. https://github.com/twitter/secureheaders/pull/339 Thanks @carlosantoniodasilva! + ## 3.6.5 Update clear-site-data header to use current format specified by the specification. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index a02a1b7d..1403245f 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "4.0.0.alpha04" + gem.version = "4.0.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 4940d4f3b4f30a0cd71ae904081528e73b365d10 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 22 Sep 2017 10:53:50 -1000 Subject: [PATCH 429/636] Remove examples from README that are never meant to be copy/pasted --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index 5d97020e..0b578619 100644 --- a/README.md +++ b/README.md @@ -74,17 +74,6 @@ SecureHeaders::Configuration.default do |config| config.x_download_options = "noopen" config.x_permitted_cross_domain_policies = "none" config.referrer_policy = "origin-when-cross-origin" - config.clear_site_data = [ - "cache", - "cookies", - "storage", - "executionContexts" - ] - config.expect_certificate_transparency = { - enforce: false, - max_age: 1.day.to_i, - report_uri: "https://report-uri.io/example-ct" - } config.csp = { # "meta" values. these will shape the header, but the values are not included in the header. preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. @@ -114,16 +103,6 @@ SecureHeaders::Configuration.default do |config| img_src: %w(somewhereelse.com), report_uri: %w(https://report-uri.io/example-csp-report-only) }) - config.hpkp = { - report_only: false, - max_age: 60.days.to_i, - include_subdomains: true, - report_uri: "https://report-uri.io/example-hpkp", - pins: [ - {sha256: "abc"}, - {sha256: "123"} - ] - } end ``` From de6a989231c246c9ed4c483e6549d6ba06a46e15 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 22 Sep 2017 11:00:46 -1000 Subject: [PATCH 430/636] Remove `includeSubdomains` in the example that's getting copy/pasted --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b578619..3f8438d4 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ SecureHeaders::Configuration.default do |config| } } # Add "; preload" and submit the site to hstspreload.org for best protection. - config.hsts = "max-age=#{20.years.to_i}; includeSubdomains" + config.hsts = "max-age=#{20.years.to_i}" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" config.x_xss_protection = "1; mode=block" From 8745cfa11ec35c5d97d4346c2ce5265f7b605afb Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Mon, 25 Sep 2017 09:52:22 +1000 Subject: [PATCH 431/636] Use `SecureHeaders` namespace for OPT_OUT docs The documentation for upgrading to 4.0 suggests using `OPT_OUT` for cookie values you don't wish to set however this throws a `NameError` ``` NameError: uninitialized constant OPT_OUT ``` To correctly resolve the `OPT_OUT` value, we need to use the `SecureHeaders` namespace. --- upgrading-to-4-0.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index d0753054..ac0ae7a8 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -4,14 +4,14 @@ The most likely change to break your app is the new cookie defaults. This is the ## All cookies default to secure/httponly/SameSite=Lax -By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs. Setting these values to `false` will raise an error. +By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `SecureHeaders::OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs. Setting these values to `false` will raise an error. ```ruby # specific opt outs config.cookies = { - secure: OPT_OUT, - httponly: OPT_OUT, - samesite: OPT_OUT, + secure: SecureHeaders::OPT_OUT, + httponly: SecureHeaders::OPT_OUT, + samesite: SecureHeaders::OPT_OUT, } # nuclear option, just make things work again From 90ac658bc8eb2e6d787f83d5da94c4f12eecdd96 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Mon, 25 Sep 2017 12:00:57 +1000 Subject: [PATCH 432/636] Add last missing `SecureHeaders` namespace fix for `OPT_OUT` I missed this in #361 but didn't notice until _after_ it was merged. --- upgrading-to-4-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upgrading-to-4-0.md b/upgrading-to-4-0.md index ac0ae7a8..61060bd1 100644 --- a/upgrading-to-4-0.md +++ b/upgrading-to-4-0.md @@ -15,7 +15,7 @@ config.cookies = { } # nuclear option, just make things work again -config.cookies = OPT_OUT +config.cookies = SecureHeaders::OPT_OUT ``` ## script_src must be set From 4bc4b746accf92196c5f8e987df231e01cba0333 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 3 Oct 2017 19:04:04 +0200 Subject: [PATCH 433/636] Support worker-src CSP directive (#364) * Support worker-src CSP directive * Add newline after `worker_src` const def * Remove trailing whitespace --- README.md | 1 + .../headers/content_security_policy_config.rb | 1 + lib/secure_headers/headers/policy_management.rb | 6 ++++++ .../secure_headers/headers/content_security_policy_spec.rb | 4 ++-- spec/lib/secure_headers/headers/policy_management_spec.rb | 1 + 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f8438d4..f0dde8d3 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ SecureHeaders::Configuration.default do |config| plugin_types: %w(application/x-shockwave-flash), script_src: %w('self'), style_src: %w('unsafe-inline'), + worker_src: %w('self'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) } diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 0227feca..44f276e6 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -38,6 +38,7 @@ def initialize(hash) @script_src = nil @style_nonce = nil @style_src = nil + @worker_src = nil @upgrade_insecure_requests = nil from_hash(hash) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 47771924..a637d19d 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -72,10 +72,13 @@ def self.included(base) BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content MANIFEST_SRC = :manifest_src UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests + WORKER_SRC = :worker_src + DIRECTIVES_3_0 = [ DIRECTIVES_2_0, BLOCK_ALL_MIXED_CONTENT, MANIFEST_SRC, + WORKER_SRC, UPGRADE_INSECURE_REQUESTS ].flatten.freeze @@ -86,6 +89,7 @@ def self.included(base) FIREFOX_UNSUPPORTED_DIRECTIVES = [ BLOCK_ALL_MIXED_CONTENT, CHILD_SRC, + WORKER_SRC, PLUGIN_TYPES ].freeze @@ -95,6 +99,7 @@ def self.included(base) FIREFOX_46_UNSUPPORTED_DIRECTIVES = [ BLOCK_ALL_MIXED_CONTENT, + WORKER_SRC, PLUGIN_TYPES ].freeze @@ -148,6 +153,7 @@ def self.included(base) SANDBOX => :sandbox_list, SCRIPT_SRC => :source_list, STYLE_SRC => :source_list, + WORKER_SRC => :source_list, UPGRADE_INSECURE_REQUESTS => :boolean }.freeze diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 44e0c5fa..2f190607 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -143,12 +143,12 @@ module SecureHeaders it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com") end it "does not filter any directives for Opera" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 9d7af025..24d35cbc 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -33,6 +33,7 @@ module SecureHeaders object_src: %w('self'), script_src: %w('self'), style_src: %w('unsafe-inline'), + worker_src: %w(worker.com), base_uri: %w('self'), form_action: %w('self' github.com), frame_ancestors: %w('none'), From 483f4d7f1c60c3a5ec3829c018ec186923a8077d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 3 Oct 2017 07:06:25 -1000 Subject: [PATCH 434/636] changelog and bump to 4.0.1 --- CHANGELOG.md | 10 +++++++++- secure_headers.gemspec | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36604f0c..242519ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ -## 4.x +## 4.0.1 + +- Adds support for `worker-src` CSP directive to 4.x line (https://github.com/twitter/secureheaders/pull/364) + +## 4.0 - See the [upgrading to 4.0](upgrading-to-4-0.md) guide. Lots of breaking changes. +## 3.7.2 + +- Adds support for `worker-src` CSP directive to 3.x line (https://github.com/twitter/secureheaders/pull/364) + ## 3.7.1 Fix support for the sandbox attribute of CSP. `true` and `[]` represent the maximally restricted policy (`sandbox;`) and validate other values. diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 1403245f..443ec16e 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "4.0.0" + gem.version = "4.0.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 4e1b8c2654feec429a207422dd3ff1d5026455fe Mon Sep 17 00:00:00 2001 From: Adam Panzer Date: Wed, 4 Oct 2017 11:42:24 -0700 Subject: [PATCH 435/636] Cookie config needs a default (#366) * Give cookies a default config #365 * An extra test * Test to make sure I can also be explicit * Update readme --- README.md | 2 +- lib/secure_headers/configuration.rb | 2 +- lib/secure_headers/middleware.rb | 2 +- spec/lib/secure_headers/configuration_spec.rb | 22 +++++++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f0dde8d3..5b470da6 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The gem will automatically apply several headers that are related to security. - Expect-CT - Only use certificates that are present in the certificate transparency logs. [Expect-CT draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/). - Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/). -It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so). +It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes. This is on default but can be turned off by using `config.cookies = SecureHeaders::OPT_OUT`. `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings. diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 92032de1..1ef2d33a 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -132,7 +132,7 @@ def deep_copy_if_hash(value) end def initialize(&block) - @cookies = nil + @cookies = self.class.send(:deep_copy_if_hash, Cookie::COOKIE_DEFAULTS) @clear_site_data = nil @csp = nil @csp_report_only = nil diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index 910c386c..ece1a7df 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -17,7 +17,7 @@ def call(env) Kernel.warn(HPKP_SAME_HOST_WARNING) end - flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies + flag_cookies!(headers, override_secure(env, config.cookies)) unless config.cookies == OPT_OUT headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index ed54bb2b..ae8c80ac 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -91,5 +91,27 @@ module SecureHeaders end }.to raise_error(ArgumentError) end + + it "gives cookies a default config" do + expect(Configuration.default.cookies).to eq({httponly: true, secure: true, samesite: {lax: true}}) + end + + it "allows OPT_OUT" do + Configuration.default do |config| + config.cookies = OPT_OUT + end + + config = Configuration.get + expect(config.cookies).to eq(OPT_OUT) + end + + it "allows me to be explicit too" do + Configuration.default do |config| + config.cookies = {httponly: true, secure: true, samesite: {lax: false}} + end + + config = Configuration.get + expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}}) + end end end From cc3d97b1da1a8b3f6d89e953f47bb01f8a4c2ecd Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 4 Oct 2017 08:48:53 -1000 Subject: [PATCH 436/636] Bump to 5.0.0 and update docs with upgrade guide --- CHANGELOG.md | 8 +++++-- README.md | 6 ++--- .../upgrading-to-3-0.md | 0 .../upgrading-to-4-0.md | 22 +------------------ docs/upgrading-to-5-0.md | 15 +++++++++++++ secure_headers.gemspec | 4 ++-- 6 files changed, 27 insertions(+), 28 deletions(-) rename upgrading-to-3-0.md => docs/upgrading-to-3-0.md (100%) rename upgrading-to-4-0.md => docs/upgrading-to-4-0.md (60%) create mode 100644 docs/upgrading-to-5-0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 242519ad..c565e65e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ +## 5.0.0 + +Well this is a little embarassing. 4.0 was supposed to set the secure/httponly/samesite=lax attributes on cookies by default but it didn't. Now it does. - See the [upgrading to 5.0](docs/upgrading-to-5-0.md) guide. + ## 4.0.1 - Adds support for `worker-src` CSP directive to 4.x line (https://github.com/twitter/secureheaders/pull/364) ## 4.0 -- See the [upgrading to 4.0](upgrading-to-4-0.md) guide. Lots of breaking changes. +- See the [upgrading to 4.0](docs/upgrading-to-4-0.md) guide. Lots of breaking changes. ## 3.7.2 @@ -322,7 +326,7 @@ Adds `upgrade-insecure-requests` support for requests from Firefox and Chrome (a ## 3.0.0 -secure_headers 3.0.0 is a near-complete, not-entirely-backward-compatible rewrite. Please see the [upgrade guide](https://github.com/twitter/secureheaders/blob/master/upgrading-to-3-0.md) for an in-depth explanation of the changes and the suggested upgrade path. +secure_headers 3.0.0 is a near-complete, not-entirely-backward-compatible rewrite. Please see the [upgrade guide](https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-3-0.md) for an in-depth explanation of the changes and the suggested upgrade path. ## 2.5.1 - 2016-02-16 18:11:11 UTC - Remove noisy deprecation warning diff --git a/README.md b/README.md index 5b470da6..edefa2dd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) -**master represents the unreleased 4.x line**. See the [upgrading to 4.x doc](upgrading-to-4-0.md) for instructions on how to upgrade. Bug fixes should go in the 3.x branch for now. +**master represents 5.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md) and [upgrading to 5.x doc](docs/upgrading-to-5-0.md) for instructions on how to upgrade. Bug fixes should go in the 3.x branch for now. -**The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. +**The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](docs/upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. **The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be not be maintained once 4.x is released**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. @@ -55,7 +55,7 @@ If you do not supply a `default` configuration, exceptions will be raised. If yo All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. -**Word of caution:** The following is not a default configuration per se. It serves as a sample implementation of the configuration. You should read more about these headers and determine what is appropriate for your requirements. +**Word of caution:** The following is not a default configuration per se. It serves as a sample implementation of the configuration. You should read more about these headers and determine what is appropriate for your requirements. ```ruby SecureHeaders::Configuration.default do |config| diff --git a/upgrading-to-3-0.md b/docs/upgrading-to-3-0.md similarity index 100% rename from upgrading-to-3-0.md rename to docs/upgrading-to-3-0.md diff --git a/upgrading-to-4-0.md b/docs/upgrading-to-4-0.md similarity index 60% rename from upgrading-to-4-0.md rename to docs/upgrading-to-4-0.md index 61060bd1..f9d6a2bb 100644 --- a/upgrading-to-4-0.md +++ b/docs/upgrading-to-4-0.md @@ -1,26 +1,6 @@ -### Breaking Changes - -The most likely change to break your app is the new cookie defaults. This is the first place to check. If you're using the default CSP, your policy will change but your app should not break. This should not break brand new projects using secure_headers either. - -## All cookies default to secure/httponly/SameSite=Lax - -By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `SecureHeaders::OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs. Setting these values to `false` will raise an error. - -```ruby -# specific opt outs -config.cookies = { - secure: SecureHeaders::OPT_OUT, - httponly: SecureHeaders::OPT_OUT, - samesite: SecureHeaders::OPT_OUT, -} - -# nuclear option, just make things work again -config.cookies = SecureHeaders::OPT_OUT -``` - ## script_src must be set -Not setting a `script_src` value means your policy falls back to whatever `default_src` (also required) is set to. This can be very dangerous and indicates the policy is too loose. +Not setting a `script_src` value means your policy falls back to whatever `default_src` (also required) is set to. This can be very dangerous and indicates the policy is too loose. However, sometimes you really don't need a `script-src` e.g. API responses (`default-src 'none'`) so you can set `script_src: SecureHeaders::OPT_OUT` to work around this. diff --git a/docs/upgrading-to-5-0.md b/docs/upgrading-to-5-0.md new file mode 100644 index 00000000..1a056781 --- /dev/null +++ b/docs/upgrading-to-5-0.md @@ -0,0 +1,15 @@ +## All cookies default to secure/httponly/SameSite=Lax + +By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `SecureHeaders::OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs. Setting these values to `false` will raise an error. + +```ruby +# specific opt outs +config.cookies = { + secure: SecureHeaders::OPT_OUT, + httponly: SecureHeaders::OPT_OUT, + samesite: SecureHeaders::OPT_OUT, +} + +# nuclear option, just make things work again +config.cookies = SecureHeaders::OPT_OUT +``` diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 443ec16e..2dd516c7 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "4.0.1" + gem.version = "5.0.0.alpha01" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." @@ -22,7 +22,7 @@ Gem::Specification.new do |gem| gem.post_install_message = <<-POST_INSTALL ********** -:wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md +:wave: secure_headers 5.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-4-0.md https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-4-0.md ********** POST_INSTALL From 7c525e42382e262403292bd42232e0c90bb49804 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Thu, 19 Oct 2017 10:09:16 -0600 Subject: [PATCH 437/636] Update Expect-CT to use separator in spec --- .../headers/expect_certificate_transparency.rb | 2 +- ...cate_spec.rb => expect_certificate_transparency_spec.rb} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename spec/lib/secure_headers/headers/{expect_certificate_spec.rb => expect_certificate_transparency_spec.rb} (90%) diff --git a/lib/secure_headers/headers/expect_certificate_transparency.rb b/lib/secure_headers/headers/expect_certificate_transparency.rb index bda4d937..d88c6541 100644 --- a/lib/secure_headers/headers/expect_certificate_transparency.rb +++ b/lib/secure_headers/headers/expect_certificate_transparency.rb @@ -49,7 +49,7 @@ def value enforced_directive, max_age_directive, report_uri_directive - ].compact.join("; ").strip + ].compact.join(", ").strip end def enforced_directive diff --git a/spec/lib/secure_headers/headers/expect_certificate_spec.rb b/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb similarity index 90% rename from spec/lib/secure_headers/headers/expect_certificate_spec.rb rename to spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb index 86735d79..640fa2e9 100644 --- a/spec/lib/secure_headers/headers/expect_certificate_spec.rb +++ b/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb @@ -3,13 +3,13 @@ module SecureHeaders describe ExpectCertificateTransparency do - specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce; max-age=1234") } + specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce, max-age=1234") } specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") } specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") } - specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"") } + specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234, report-uri=\"https://report-uri.io/expect-ct\"") } specify do config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" } - header_value = "enforce; max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"" + header_value = "enforce, max-age=1234, report-uri=\"https://report-uri.io/expect-ct\"" expect(ExpectCertificateTransparency.new(config).value).to eq(header_value) end From 0190de96e3b1426a4819d1e9b788b1a7e54253bf Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Thu, 19 Oct 2017 10:13:24 -0600 Subject: [PATCH 438/636] Updates CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c565e65e..d857b33c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.1 + +- Updates `Expect-CT` header to use a comma separator between directives, as specified in the most current spec. + ## 5.0.0 Well this is a little embarassing. 4.0 was supposed to set the secure/httponly/samesite=lax attributes on cookies by default but it didn't. Now it does. - See the [upgrading to 5.0](docs/upgrading-to-5-0.md) guide. From c9b2c7c511aa74b5593626cdb34984c815f49005 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 19 Oct 2017 07:53:07 -1000 Subject: [PATCH 439/636] bump to 5.0.1 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 2dd516c7..d8f83412 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "5.0.0.alpha01" + gem.version = "5.0.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From e2115caa8f12138d859a52ee4e84b549e235d520 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 27 Oct 2017 21:21:40 -1000 Subject: [PATCH 440/636] Delete HPKP.md --- docs/HPKP.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 docs/HPKP.md diff --git a/docs/HPKP.md b/docs/HPKP.md deleted file mode 100644 index 9428d0d9..00000000 --- a/docs/HPKP.md +++ /dev/null @@ -1,17 +0,0 @@ -## HTTP Public Key Pins - -Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work. - -```ruby -config.hpkp = { - max_age: 60.days.to_i, # max_age is a required parameter - include_subdomains: true, # whether or not to apply pins to subdomains - # Per the spec, SHA256 hashes are the only currently supported format. - pins: [ - {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, - {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} - ], - report_only: true, # defaults to false (report-only mode) - report_uri: 'https://report-uri.io/example-hpkp' -} -``` From 92ba4c82a8c55adda65d6b4c36b3794be144cbde Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 27 Oct 2017 21:22:16 -1000 Subject: [PATCH 441/636] remove hpkp from readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index edefa2dd..7d42dd65 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ The gem will automatically apply several headers that are related to security. - X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/) -- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469) - Expect-CT - Only use certificates that are present in the certificate transparency logs. [Expect-CT draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/). - Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/). @@ -31,7 +30,6 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu - [Named overrides and appends](docs/named_overrides_and_appends.md) - [Per action configuration](docs/per_action_configuration.md) - [Cookies](docs/cookies.md) -- [HPKP](docs/HPKP.md) - [Hashes](docs/hashes.md) - [Sinatra Config](docs/sinatra.md) From f738658585b94f10ecd32977644de80b80008b8b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 27 Oct 2017 21:22:43 -1000 Subject: [PATCH 442/636] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d42dd65..4ecd803a 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ SecureHeaders::Configuration.default do |config| } } # Add "; preload" and submit the site to hstspreload.org for best protection. - config.hsts = "max-age=#{20.years.to_i}" + config.hsts = "max-age=#{1.week.to_i}" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" config.x_xss_protection = "1; mode=block" From 41e87d545cc823b32eb3b7651b28721ea257dd13 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 14 Nov 2017 12:54:18 -0700 Subject: [PATCH 443/636] Add support for multiple policies --- lib/secure_headers/headers/referrer_policy.rb | 17 +++++++++++----- .../headers/referrer_policy_spec.rb | 20 ++++++++++++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index 7cd69479..343187bc 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -22,14 +22,21 @@ class << self # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil) - [HEADER_NAME, config || DEFAULT_VALUE] + config ||= DEFAULT_VALUE + [HEADER_NAME, Array(config).join(", ")] end def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) - unless VALID_POLICIES.include?(config.downcase) - raise ReferrerPolicyConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") + case config + when nil, OPT_OUT + # valid + when String, Array + config = Array(config) + unless config.all? { |t| t.is_a?(String) && VALID_POLICIES.include?(t.downcase) } + raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(', ')}") + end + else + raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}") end end end diff --git a/spec/lib/secure_headers/headers/referrer_policy_spec.rb b/spec/lib/secure_headers/headers/referrer_policy_spec.rb index 96dbd62d..fad345fa 100644 --- a/spec/lib/secure_headers/headers/referrer_policy_spec.rb +++ b/spec/lib/secure_headers/headers/referrer_policy_spec.rb @@ -5,6 +5,7 @@ module SecureHeaders describe ReferrerPolicy do specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) } specify { expect(ReferrerPolicy.make_header("no-referrer")).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) } + specify { expect(ReferrerPolicy.make_header(%w(origin-when-cross-origin strict-origin-when-cross-origin))).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin, strict-origin-when-cross-origin"]) } context "valid configuration values" do it "accepts 'no-referrer'" do @@ -60,14 +61,31 @@ module SecureHeaders ReferrerPolicy.validate_config!(nil) end.not_to raise_error end + + it "accepts array of policy values" do + expect do + ReferrerPolicy.validate_config!( + %w( + origin-when-cross-origin + strict-origin-when-cross-origin + ) + ) + end.not_to raise_error + end end - context "invlaid configuration values" do + context "invalid configuration values" do it "doesn't accept invalid values" do expect do ReferrerPolicy.validate_config!("open") end.to raise_error(ReferrerPolicyConfigError) end + + it "doesn't accept invalid types" do + expect do + ReferrerPolicy.validate_config!({}) + end.to raise_error(TypeError) + end end end end From 8447ec32c552034c48498fd782c382f1df3f0a6d Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 14 Nov 2017 12:58:06 -0700 Subject: [PATCH 444/636] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d857b33c..c0eb61e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.2 + +- Updates `Referrer-Policy` header to support multiple policy values + ## 5.0.1 - Updates `Expect-CT` header to use a comma separator between directives, as specified in the most current spec. From 9e04fa34d7d6f93b5cac5d727300347e3cbd2e98 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 14 Nov 2017 12:59:26 -0700 Subject: [PATCH 445/636] Update example to use multiple config values --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ecd803a..d79d386d 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ SecureHeaders::Configuration.default do |config| config.x_xss_protection = "1; mode=block" config.x_download_options = "noopen" config.x_permitted_cross_domain_policies = "none" - config.referrer_policy = "origin-when-cross-origin" + config.referrer_policy = %w(origin-when-cross-origin strict-origin-when-cross-origin) config.csp = { # "meta" values. these will shape the header, but the values are not included in the header. preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. From 0474176a5bc0f4e5ffada1d9d83a9efd09cbabb4 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 14 Nov 2017 13:22:20 -0700 Subject: [PATCH 446/636] Use consistent quotes throughout --- lib/secure_headers/headers/referrer_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index 343187bc..e91e4331 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -33,7 +33,7 @@ def validate_config!(config) when String, Array config = Array(config) unless config.all? { |t| t.is_a?(String) && VALID_POLICIES.include?(t.downcase) } - raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(', ')}") + raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(", ")}") end else raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}") From d34fa279d4b43c9c6c0e4e3becb9db1db103caa7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 14 Nov 2017 10:52:28 -1000 Subject: [PATCH 447/636] bump to 5.0.2 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index d8f83412..dcd3f4af 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "5.0.1" + gem.version = "5.0.2" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 1e81a0a5635647618f5678123cefbb76baad0c6e Mon Sep 17 00:00:00 2001 From: Steve Hoeksema Date: Wed, 15 Nov 2017 11:49:39 +1300 Subject: [PATCH 448/636] Add nonced versions of Rails link/include tags Adds support for nonced versions of Rails' CSS, JavaScript and Webpacker link/include tags. * nonced_stylesheet_link_tag * nonced_javascript_include_tag * nonced_javascript_pack_tag --- docs/per_action_configuration.md | 6 +++++ lib/secure_headers/view_helper.rb | 28 ++++++++++++++++++-- spec/lib/secure_headers/view_helpers_spec.rb | 16 +++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/per_action_configuration.md b/docs/per_action_configuration.md index b8750d31..8556540e 100644 --- a/docs/per_action_configuration.md +++ b/docs/per_action_configuration.md @@ -66,6 +66,12 @@ body { background-color: black; } <% end %> + +<%= nonced_javascript_include_tag "include.js" %> + +<%= nonced_javascript_pack_tag "pack.js" %> + +<%= nonced_stylesheet_link_tag "link.css" %> ``` becomes: diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 5353c518..257b990e 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -7,21 +7,45 @@ module ViewHelpers class UnexpectedHashedScriptException < StandardError; end # Public: create a style tag using the content security policy nonce. - # Instructs secure_headers to append a nonce to style/script-src directives. + # Instructs secure_headers to append a nonce to style-src directive. # # Returns an html-safe style tag with the nonce attribute. def nonced_style_tag(content_or_options = {}, &block) nonced_tag(:style, content_or_options, block) end + # Public: create a stylesheet link tag using the content security policy nonce. + # Instructs secure_headers to append a nonce to style-src directive. + # + # Returns an html-safe link tag with the nonce attribute. + def nonced_stylesheet_link_tag(*args, &block) + stylesheet_link_tag(*args, nonce: content_security_policy_nonce(:style), &block) + end + # Public: create a script tag using the content security policy nonce. - # Instructs secure_headers to append a nonce to style/script-src directives. + # Instructs secure_headers to append a nonce to script-src directive. # # Returns an html-safe script tag with the nonce attribute. def nonced_javascript_tag(content_or_options = {}, &block) nonced_tag(:script, content_or_options, block) end + # Public: create a script src tag using the content security policy nonce. + # Instructs secure_headers to append a nonce to script-src directive. + # + # Returns an html-safe script tag with the nonce attribute. + def nonced_javascript_include_tag(*args, &block) + javascript_include_tag(*args, nonce: content_security_policy_nonce(:script), &block) + end + + # Public: create a script Webpacker pack tag using the content security policy nonce. + # Instructs secure_headers to append a nonce to script-src directive. + # + # Returns an html-safe script tag with the nonce attribute. + def nonced_javascript_pack_tag(*args, &block) + javascript_pack_tag(*args, nonce: content_security_policy_nonce(:script), &block) + end + # Public: use the content security policy nonce for this request directly. # Instructs secure_headers to append a nonce to style/script-src directives. # diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 5fae59b6..a75a3058 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -39,6 +39,12 @@ def self.template } +<%= nonced_javascript_include_tag "include.js" %> + +<%= nonced_javascript_pack_tag "pack.js" %> + +<%= nonced_stylesheet_link_tag "link.css" %> + TEMPLATE end @@ -64,6 +70,16 @@ def content_tag(type, content = nil, options = nil, &block) "<#{type}#{options}>#{content}" end + def javascript_include_tag(source, options = {}) + content_tag(:script, nil, options.merge(src: source)) + end + + alias_method :javascript_pack_tag, :javascript_include_tag + + def stylesheet_link_tag(source, options = {}) + content_tag(:link, nil, options.merge(href: source, rel: "stylesheet", media: "screen")) + end + def result super(binding) end From ec4b50e101e45bd0ecea6b9b210cc9e528493b8f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 20 Nov 2017 09:41:24 -1000 Subject: [PATCH 449/636] bump to 5.0.3 --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0eb61e1..bbbea081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.3 + +- Add nonced versions of Rails link/include tags #372 (@steveh) + ## 5.0.2 - Updates `Referrer-Policy` header to support multiple policy values diff --git a/secure_headers.gemspec b/secure_headers.gemspec index dcd3f4af..5252d4ec 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "5.0.2" + gem.version = "5.0.3" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From ba575317780eca138829ab6855e38ea564ed8762 Mon Sep 17 00:00:00 2001 From: Paul Friedman Date: Tue, 21 Nov 2017 14:15:11 -0800 Subject: [PATCH 450/636] Add missing `nonced_stylesheet_pack_tag` Equivalents were added in 1e81a0a. This also fixes a bug preventing passing any arguments to these tags. Tests were updated with a sample to reflect this. --- docs/per_action_configuration.md | 4 +++- lib/secure_headers/view_helper.rb | 16 ++++++++++++---- spec/lib/secure_headers/view_helpers_spec.rb | 6 +++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/per_action_configuration.md b/docs/per_action_configuration.md index 8556540e..c17a9e5f 100644 --- a/docs/per_action_configuration.md +++ b/docs/per_action_configuration.md @@ -72,6 +72,8 @@ body { <%= nonced_javascript_pack_tag "pack.js" %> <%= nonced_stylesheet_link_tag "link.css" %> + +<%= nonced_stylesheet_pack_tag "pack.css" %> ``` becomes: @@ -136,4 +138,4 @@ end class SessionsController < ApplicationController after_action :clear_browser_cache, only: :destroy end -``` \ No newline at end of file +``` diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 257b990e..9e8f52c4 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -34,16 +34,24 @@ def nonced_javascript_tag(content_or_options = {}, &block) # Instructs secure_headers to append a nonce to script-src directive. # # Returns an html-safe script tag with the nonce attribute. - def nonced_javascript_include_tag(*args, &block) - javascript_include_tag(*args, nonce: content_security_policy_nonce(:script), &block) + def nonced_javascript_include_tag(*args, **kwargs, &block) + javascript_include_tag(*args, kwargs.merge(nonce: content_security_policy_nonce(:script)), &block) end # Public: create a script Webpacker pack tag using the content security policy nonce. # Instructs secure_headers to append a nonce to script-src directive. # # Returns an html-safe script tag with the nonce attribute. - def nonced_javascript_pack_tag(*args, &block) - javascript_pack_tag(*args, nonce: content_security_policy_nonce(:script), &block) + def nonced_javascript_pack_tag(*args, **kwargs, &block) + javascript_pack_tag(*args, kwargs.merge(nonce: content_security_policy_nonce(:script)), &block) + end + + # Public: create a stylesheet Webpacker link tag using the content security policy nonce. + # Instructs secure_headers to append a nonce to style-src directive. + # + # Returns an html-safe link tag with the nonce attribute. + def nonced_stylesheet_pack_tag(*args, **kwargs, &block) + stylesheet_pack_tag(*args, kwargs.merge(nonce: content_security_policy_nonce(:style)), &block) end # Public: use the content security policy nonce for this request directly. diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index a75a3058..85b78f20 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -41,10 +41,12 @@ def self.template <%= nonced_javascript_include_tag "include.js" %> -<%= nonced_javascript_pack_tag "pack.js" %> +<%= nonced_javascript_pack_tag "pack.js", defer: true %> <%= nonced_stylesheet_link_tag "link.css" %> +<%= nonced_stylesheet_pack_tag "pack.css", media: :all %> + TEMPLATE end @@ -80,6 +82,8 @@ def stylesheet_link_tag(source, options = {}) content_tag(:link, nil, options.merge(href: source, rel: "stylesheet", media: "screen")) end + alias_method :stylesheet_pack_tag, :stylesheet_link_tag + def result super(binding) end From 2b6519f8d964ff7dd519ea3b922622cf126f261d Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Fri, 24 Nov 2017 10:26:42 +1000 Subject: [PATCH 451/636] the same URL is repeated twice in the gemspec --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 5252d4ec..cda1f4e4 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |gem| gem.post_install_message = <<-POST_INSTALL ********** -:wave: secure_headers 5.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-4-0.md https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-4-0.md +:wave: secure_headers 5.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-4-0.md ********** POST_INSTALL From 3b43af51ed17d9473fef403789d0440be287e7bb Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Sat, 25 Nov 2017 17:10:37 +1000 Subject: [PATCH 452/636] correct url --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index cda1f4e4..81ddfead 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |gem| gem.post_install_message = <<-POST_INSTALL ********** -:wave: secure_headers 5.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-4-0.md +:wave: secure_headers 5.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-5-0.md ********** POST_INSTALL From 2662fc48b53b6ef14aa2aa9598b0bdfeefb4c556 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 29 Nov 2017 08:15:50 -1000 Subject: [PATCH 453/636] upgrade some test dependencies --- Gemfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 111b8c8b..0a64a943 100644 --- a/Gemfile +++ b/Gemfile @@ -5,14 +5,14 @@ gemspec group :test do gem "coveralls" - gem "json", "~> 1" + gem "json" gem "pry-nav" - gem "rack", "~> 1" + gem "rack" gem "rspec" - gem "rubocop", "~> 0.47.0" + gem "rubocop" gem "rubocop-github" - gem "term-ansicolor", "< 1.4" - gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0 + gem "term-ansicolor" + gem "tins" end group :guard do From 17cd26c3e93540489f70ce0727ed3ef098ab5793 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 29 Nov 2017 08:18:22 -1000 Subject: [PATCH 454/636] rubocop --- lib/secure_headers/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 1ef2d33a..0998377e 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -308,7 +308,7 @@ def generate_csp_headers(headers) def generate_csp_headers_for_config(headers, header_key, csp_config) unless csp_config.opt_out? headers[header_key] = {} - ContentSecurityPolicy::VARIATIONS.each do |name, _| + ContentSecurityPolicy::VARIATIONS.each_key do |name| csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name)) headers[header_key][name] = csp.freeze end From 8a784a35e4ebd2760963554d21e77f318b3220a3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 29 Nov 2017 08:23:28 -1000 Subject: [PATCH 455/636] upgrade ruby while we're at it --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 005119ba..8e8299dc 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.1 +2.4.2 From 08253208d37913aa943d61f7aa5b953d0fe8c231 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 29 Nov 2017 08:25:32 -1000 Subject: [PATCH 456/636] will this use the current 2.4.2/2.3.5? --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1f092bb6..f63b693d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: ruby rvm: - ruby-head - - 2.4.1 - - 2.3.4 + - 2.4 + - 2.3 - 2.2 - jruby-head From c130007adf0edbe87df59e6ec73932da97c54b50 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 29 Nov 2017 08:27:30 -1000 Subject: [PATCH 457/636] nope, gotta be specific :thinking_face: --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index f63b693d..30723457 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,9 @@ language: ruby rvm: - ruby-head - - 2.4 - - 2.3 - - 2.2 + - 2.4.2 + - 2.3.5 + - 2.2.8 - jruby-head env: From 35a505df70b8a52fd87ecb85ead97117a5560027 Mon Sep 17 00:00:00 2001 From: Paul Friedman Date: Wed, 29 Nov 2017 12:38:44 -0800 Subject: [PATCH 458/636] Support multiple sources in asset tag helpers --- lib/secure_headers/view_helper.rb | 30 +++++++++++++++----- spec/lib/secure_headers/view_helpers_spec.rb | 20 +++++++------ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 9e8f52c4..0299fec0 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -19,7 +19,9 @@ def nonced_style_tag(content_or_options = {}, &block) # # Returns an html-safe link tag with the nonce attribute. def nonced_stylesheet_link_tag(*args, &block) - stylesheet_link_tag(*args, nonce: content_security_policy_nonce(:style), &block) + opts = extract_options(args).merge(nonce: content_security_policy_nonce(:style)) + + stylesheet_link_tag(*args, opts, &block) end # Public: create a script tag using the content security policy nonce. @@ -34,24 +36,30 @@ def nonced_javascript_tag(content_or_options = {}, &block) # Instructs secure_headers to append a nonce to script-src directive. # # Returns an html-safe script tag with the nonce attribute. - def nonced_javascript_include_tag(*args, **kwargs, &block) - javascript_include_tag(*args, kwargs.merge(nonce: content_security_policy_nonce(:script)), &block) + def nonced_javascript_include_tag(*args, &block) + opts = extract_options(args).merge(nonce: content_security_policy_nonce(:script)) + + javascript_include_tag(*args, opts, &block) end # Public: create a script Webpacker pack tag using the content security policy nonce. # Instructs secure_headers to append a nonce to script-src directive. # # Returns an html-safe script tag with the nonce attribute. - def nonced_javascript_pack_tag(*args, **kwargs, &block) - javascript_pack_tag(*args, kwargs.merge(nonce: content_security_policy_nonce(:script)), &block) + def nonced_javascript_pack_tag(*args, &block) + opts = extract_options(args).merge(nonce: content_security_policy_nonce(:script)) + + javascript_pack_tag(*args, opts, &block) end # Public: create a stylesheet Webpacker link tag using the content security policy nonce. # Instructs secure_headers to append a nonce to style-src directive. # # Returns an html-safe link tag with the nonce attribute. - def nonced_stylesheet_pack_tag(*args, **kwargs, &block) - stylesheet_pack_tag(*args, kwargs.merge(nonce: content_security_policy_nonce(:style)), &block) + def nonced_stylesheet_pack_tag(*args, &block) + opts = extract_options(args).merge(nonce: content_security_policy_nonce(:style)) + + stylesheet_pack_tag(*args, opts, &block) end # Public: use the content security policy nonce for this request directly. @@ -146,6 +154,14 @@ def nonced_tag(type, content_or_options, block) end content_tag type, content, options.merge(nonce: content_security_policy_nonce(type)) end + + def extract_options(args) + if args.last.is_a? Hash + args.pop + else + {} + end + end end end diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 85b78f20..54387dbf 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -39,13 +39,13 @@ def self.template } -<%= nonced_javascript_include_tag "include.js" %> +<%= nonced_javascript_include_tag "include.js", defer: true %> -<%= nonced_javascript_pack_tag "pack.js", defer: true %> +<%= nonced_javascript_pack_tag "pack.js", "otherpack.js", defer: true %> -<%= nonced_stylesheet_link_tag "link.css" %> +<%= nonced_stylesheet_link_tag "link.css", media: :all %> -<%= nonced_stylesheet_pack_tag "pack.css", media: :all %> +<%= nonced_stylesheet_pack_tag "pack.css", "otherpack.css", media: :all %> TEMPLATE end @@ -72,14 +72,18 @@ def content_tag(type, content = nil, options = nil, &block) "<#{type}#{options}>#{content}" end - def javascript_include_tag(source, options = {}) - content_tag(:script, nil, options.merge(src: source)) + def javascript_include_tag(*sources, **options) + sources.map do |source| + content_tag(:script, nil, options.merge(src: source)) + end end alias_method :javascript_pack_tag, :javascript_include_tag - def stylesheet_link_tag(source, options = {}) - content_tag(:link, nil, options.merge(href: source, rel: "stylesheet", media: "screen")) + def stylesheet_link_tag(*sources, **options) + sources.map do |source| + content_tag(:link, nil, options.merge(href: source, rel: "stylesheet", media: "screen")) + end end alias_method :stylesheet_pack_tag, :stylesheet_link_tag From c7fc44c3e574c5a4b125cf6c06f1aa6dc6b6aa77 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 5 Dec 2017 08:29:52 -1000 Subject: [PATCH 459/636] bump to 5.0.4 --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbbea081..0a693690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.4 + +- Adds support for `nonced_stylesheet_pack_tag` #373 (@paulfri) + ## 5.0.3 - Add nonced versions of Rails link/include tags #372 (@steveh) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 81ddfead..00f473b0 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "5.0.3" + gem.version = "5.0.4" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From d300d6f9f5ce954b70c69a2a78e4fc9bb2024b66 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Fri, 19 Jan 2018 16:58:47 -0700 Subject: [PATCH 460/636] Remove caching and make overrides work as expected We ran into a bug where overrides weren't working as expected. The user expectation is that an override should override a config value for whatever the current configuration for the current request. But, as implemented, they actually were just forks of a base config..regardless of whether that base config changed during runtime. So, overrides no work that way and we don't currently support any way of using an alternate config. This might be added back later if it is useful. While making the changes related to overrides it became obvious that caching was a huge source of complexity. So, this also removes all caching. A quick local benchmark shows that a non-cached config takes about .18ms to build out a set of headers, so it shouldn't be a big concern. --- lib/secure_headers.rb | 83 ++------- lib/secure_headers/configuration.rb | 161 +++++++----------- lib/secure_headers/headers/clear_site_data.rb | 4 +- .../headers/content_security_policy_config.rb | 10 -- .../expect_certificate_transparency.rb | 4 +- .../headers/policy_management.rb | 1 + lib/secure_headers/headers/public_key_pins.rb | 4 +- lib/secure_headers/headers/referrer_policy.rb | 3 +- .../headers/strict_transport_security.rb | 3 +- .../headers/x_content_type_options.rb | 3 +- .../headers/x_download_options.rb | 3 +- lib/secure_headers/headers/x_frame_options.rb | 2 +- .../x_permitted_cross_domain_policies.rb | 3 +- .../headers/x_xss_protection.rb | 3 +- spec/lib/secure_headers/configuration_spec.rb | 68 +------- spec/lib/secure_headers/middleware_spec.rb | 3 +- spec/lib/secure_headers/view_helpers_spec.rb | 2 +- spec/lib/secure_headers_spec.rb | 21 ++- 18 files changed, 113 insertions(+), 268 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index a0b7a39a..3620e184 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "secure_headers/configuration" require "secure_headers/hash_helper" require "secure_headers/headers/cookie" require "secure_headers/headers/public_key_pins" @@ -18,6 +17,7 @@ require "secure_headers/view_helper" require "useragent" require "singleton" +require "secure_headers/configuration" # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT # or ":optout_of_protection" as a config value to disable a given header @@ -52,29 +52,9 @@ def opt_out? HTTPS = "https".freeze CSP = ContentSecurityPolicy - ALL_HEADER_CLASSES = [ - ExpectCertificateTransparency, - ClearSiteData, - ContentSecurityPolicyConfig, - ContentSecurityPolicyReportOnlyConfig, - StrictTransportSecurity, - PublicKeyPins, - ReferrerPolicy, - XContentTypeOptions, - XDownloadOptions, - XFrameOptions, - XPermittedCrossDomainPolicies, - XXssProtection - ].freeze - - ALL_HEADERS_BESIDES_CSP = ( - ALL_HEADER_CLASSES - - [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig] - ).freeze - # Headers set on http requests (excludes STS and HPKP) - HTTP_HEADER_CLASSES = - (ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze + HTTPS_HEADER_CLASSES = + [StrictTransportSecurity, PublicKeyPins].freeze class << self # Public: override a given set of directives for the current request. If a @@ -153,7 +133,7 @@ def opt_out_of_header(request, header_key) # Public: opts out of setting all headers by telling secure_headers to use # the NOOP configuration. def opt_out_of_all_protection(request) - use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION) + use_secure_headers_override(request, Configuration::NOOP_OVERRIDE) end # Public: Builds the hash of headers that should be applied base on the @@ -168,27 +148,15 @@ def opt_out_of_all_protection(request) def header_hash_for(request) prevent_dup = true config = config_for(request, prevent_dup) - headers = config.cached_headers user_agent = UserAgent.parse(request.user_agent) + headers = config.generate_headers(user_agent) - if !config.csp.opt_out? && config.csp.modified? - headers = update_cached_csp(config.csp, headers, user_agent) - end - - if !config.csp_report_only.opt_out? && config.csp_report_only.modified? - headers = update_cached_csp(config.csp_report_only, headers, user_agent) - end - - header_classes_for(request).each_with_object({}) do |klass, hash| - if header = headers[klass::CONFIG_KEY] - header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig - csp_header_for_ua(header, user_agent) - else - header - end - hash[header_name] = value + if request.scheme != HTTPS + HTTPS_HEADER_CLASSES.each do |klass| + headers.delete(klass::HEADER_NAME) end end + headers end # Public: specify which named override will be used for this request. @@ -196,7 +164,9 @@ def header_hash_for(request) # # name - the name of the previously configured override. def use_secure_headers_override(request, name) - if config = Configuration.get(name) + if override = Configuration.overrides(name) + config = config_for(request) + config.instance_eval(&override) override_secure_headers_request_config(request, config) else raise ArgumentError.new("no override by the name of #{name} has been configured") @@ -285,35 +255,6 @@ def content_security_policy_nonce(request, script_or_style) def override_secure_headers_request_config(request, config) request.env[SECURE_HEADERS_CONFIG] = config end - - # Private: determines which headers are applicable to a given request. - # - # Returns a list of classes whose corresponding header values are valid for - # this request. - def header_classes_for(request) - if request.scheme == HTTPS - ALL_HEADER_CLASSES - else - HTTP_HEADER_CLASSES - end - end - - def update_cached_csp(config, headers, user_agent) - headers = Configuration.send(:deep_copy, headers) - headers[config.class::CONFIG_KEY] = {} - variation = ContentSecurityPolicy.ua_to_variation(user_agent) - headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent) - headers - end - - # Private: chooses the applicable CSP header for the provided user agent. - # - # headers - a hash of header_config_key => [header_name, header_value] - # - # Returns a CSP [header, value] array - def csp_header_for_ua(headers, user_agent) - headers[ContentSecurityPolicy.ua_to_variation(user_agent)] - end end # These methods are mixed into controllers and delegate to the class method diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 0998377e..deda74bd 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -4,7 +4,7 @@ module SecureHeaders class Configuration DEFAULT_CONFIG = :default - NOOP_CONFIGURATION = "secure_headers_noop_config" + NOOP_OVERRIDE = "secure_headers_noop_override" class NotYetConfiguredError < StandardError; end class IllegalPolicyModificationError < StandardError; end class << self @@ -15,8 +15,13 @@ class << self # Returns the newly created config. def default(&block) config = new(&block) - add_noop_configuration add_configuration(DEFAULT_CONFIG, config) + override(NOOP_OVERRIDE) do |config| + CONFIG_ATTRIBUTES.each do |attr| + config.instance_variable_set("@#{attr}", OPT_OUT) + end + end + config end alias_method :configure, :default @@ -27,13 +32,14 @@ def default(&block) # if no value is supplied. # # Returns: the newly created config - def override(name, base = DEFAULT_CONFIG, &block) - unless get(base) - raise NotYetConfiguredError, "#{base} policy not yet supplied" - end - override = @configurations[base].dup - override.instance_eval(&block) if block_given? - add_configuration(name, override) + def override(name, &block) + @overrides ||= {} + @overrides[name] = block + end + + def overrides(name) + @overrides ||= {} + @overrides[name] end # Public: retrieve a global configuration object @@ -72,25 +78,10 @@ def named_append(name, target = nil, &block) def add_configuration(name, config) config.validate_config! @configurations ||= {} - config.send(:cache_headers!) - config.send(:cache_hpkp_report_host) config.freeze @configurations[name] = config end - # Private: Automatically add an "opt-out of everything" override. - # - # Returns the noop config - def add_noop_configuration - noop_config = new do |config| - ALL_HEADER_CLASSES.each do |klass| - config.send("#{klass::CONFIG_KEY}=", OPT_OUT) - end - end - - add_configuration(NOOP_CONFIGURATION, noop_config) - end - # Public: perform a basic deep dup. The shallow copy provided by dup/clone # can lead to modifying parent objects. def deep_copy(config) @@ -115,11 +106,28 @@ def deep_copy_if_hash(value) end end - attr_writer :hsts, :x_frame_options, :x_content_type_options, - :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies, - :referrer_policy, :clear_site_data, :expect_certificate_transparency - - attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host + NON_HEADER_ATTRIBUTES = [ + :cookies, :hpkp_report_host + ].freeze + + HEADER_ATTRIBUTES_TO_HEADER_CLASSES = { + hsts: StrictTransportSecurity, + x_frame_options: XFrameOptions, + x_content_type_options: XContentTypeOptions, + x_xss_protection: XXssProtection, + x_download_options: XDownloadOptions, + x_permitted_cross_domain_policies: XPermittedCrossDomainPolicies, + referrer_policy: ReferrerPolicy, + clear_site_data: ClearSiteData, + expect_certificate_transparency: ExpectCertificateTransparency, + csp: ContentSecurityPolicy, + csp_report_only: ContentSecurityPolicy, + hpkp: PublicKeyPins, + }.freeze + + CONFIG_ATTRIBUTES = (HEADER_ATTRIBUTES_TO_HEADER_CLASSES.keys + NON_HEADER_ATTRIBUTES).freeze + + attr_accessor(*CONFIG_ATTRIBUTES) @script_hashes = nil @style_hashes = nil @@ -154,7 +162,7 @@ def initialize(&block) instance_eval(&block) if block_given? end - # Public: copy everything but the cached headers + # Public: copy everything # # Returns a deep-dup'd copy of this configuration. def dup @@ -162,7 +170,6 @@ def dup copy.cookies = self.class.send(:deep_copy_if_hash, @cookies) copy.csp = @csp.dup if @csp copy.csp_report_only = @csp_report_only.dup if @csp_report_only - copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) copy.x_content_type_options = @x_content_type_options copy.hsts = @hsts copy.x_frame_options = @x_frame_options @@ -173,18 +180,26 @@ def dup copy.expect_certificate_transparency = @expect_certificate_transparency copy.referrer_policy = @referrer_policy copy.hpkp = @hpkp - copy.hpkp_report_host = @hpkp_report_host copy end + def generate_headers(user_agent) + headers = {} + HEADER_ATTRIBUTES_TO_HEADER_CLASSES.each do |attr, klass| + header_name, value = klass.make_header(instance_variable_get("@#{attr}"), user_agent) + if header_name && value + headers[header_name] = value + end + end + headers + end + def opt_out(header) send("#{header}=", OPT_OUT) - self.cached_headers.delete(header) end def update_x_frame_options(value) @x_frame_options = value - self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value) end # Public: validates all configurations values. @@ -193,18 +208,9 @@ def update_x_frame_options(value) # # Returns nothing def validate_config! - StrictTransportSecurity.validate_config!(@hsts) - ContentSecurityPolicy.validate_config!(@csp) - ContentSecurityPolicy.validate_config!(@csp_report_only) - ReferrerPolicy.validate_config!(@referrer_policy) - XFrameOptions.validate_config!(@x_frame_options) - XContentTypeOptions.validate_config!(@x_content_type_options) - XXssProtection.validate_config!(@x_xss_protection) - XDownloadOptions.validate_config!(@x_download_options) - XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies) - ClearSiteData.validate_config!(@clear_site_data) - ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency) - PublicKeyPins.validate_config!(@hpkp) + HEADER_ATTRIBUTES_TO_HEADER_CLASSES.each do |attr, klass| + klass.validate_config!(instance_variable_get("@#{attr}")) + end Cookie.validate_config!(@cookies) end @@ -247,72 +253,19 @@ def csp_report_only=(new_csp) end end + def hpkp_report_host + return nil unless @hpkp && hpkp != OPT_OUT && @hpkp[:report_uri] + URI.parse(@hpkp[:report_uri]).host + end + protected def cookies=(cookies) @cookies = cookies end - def cached_headers=(headers) - @cached_headers = headers - end - def hpkp=(hpkp) @hpkp = self.class.send(:deep_copy_if_hash, hpkp) end - - def hpkp_report_host=(hpkp_report_host) - @hpkp_report_host = hpkp_report_host - end - - private - - def cache_hpkp_report_host - has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri] - self.hpkp_report_host = if has_report_uri - parsed_report_uri = URI.parse(@hpkp[:report_uri]) - parsed_report_uri.host - end - end - - # Public: Precompute the header names and values for this configuration. - # Ensures that headers generated at configure time, not on demand. - # - # Returns the cached headers - def cache_headers! - # generate defaults for the "easy" headers - headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash| - config = instance_variable_get("@#{klass::CONFIG_KEY}") - unless config == OPT_OUT - hash[klass::CONFIG_KEY] = klass.make_header(config).freeze - end - end - - generate_csp_headers(headers) - - headers.freeze - self.cached_headers = headers - end - - # Private: adds CSP headers for each variation of CSP support. - # - # headers - generated headers are added to this hash namespaced by The - # different variations - # - # Returns nothing - def generate_csp_headers(headers) - generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp) - generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only) - end - - def generate_csp_headers_for_config(headers, header_key, csp_config) - unless csp_config.opt_out? - headers[header_key] = {} - ContentSecurityPolicy::VARIATIONS.each_key do |name| - csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name)) - headers[header_key][name] = csp.freeze - end - end - end end end diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index 34f09d0d..6e70d985 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -17,7 +17,7 @@ class << self # Public: make an Clear-Site-Data header name, value pair # # Returns nil if not configured, returns header name and value if configured. - def make_header(config = nil) + def make_header(config = nil, user_agent = nil) case config when nil, OPT_OUT, [] # noop @@ -48,7 +48,7 @@ def validate_config!(config) # # Returns a String of quoted values that are comma separated. def make_header_value(types) - types.map { |t| "\"#{t}\""}.join(", ") + types.map { |t| "\"#{t}\"" }.join(", ") end end end diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 44f276e6..254bf9a5 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -2,7 +2,6 @@ module SecureHeaders module DynamicConfig def self.included(base) - base.send(:attr_writer, :modified) base.send(:attr_reader, *base.attrs) base.attrs.each do |attr| base.send(:define_method, "#{attr}=") do |value| @@ -42,7 +41,6 @@ def initialize(hash) @upgrade_insecure_requests = nil from_hash(hash) - @modified = false end def update_directive(directive, value) @@ -55,10 +53,6 @@ def directive_value(directive) end end - def modified? - @modified - end - def merge(new_hash) ContentSecurityPolicy.combine_policies(self.to_h, new_hash) end @@ -109,11 +103,7 @@ def from_hash(hash) def write_attribute(attr, value) value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list attr_variable = "@#{attr}" - prev_value = self.instance_variable_get(attr_variable) self.instance_variable_set(attr_variable, value) - if prev_value != value - @modified = true - end end end diff --git a/lib/secure_headers/headers/expect_certificate_transparency.rb b/lib/secure_headers/headers/expect_certificate_transparency.rb index d88c6541..c40acb58 100644 --- a/lib/secure_headers/headers/expect_certificate_transparency.rb +++ b/lib/secure_headers/headers/expect_certificate_transparency.rb @@ -15,8 +15,8 @@ class << self # # Returns nil if not configured, returns header name and value if # configured. - def make_header(config) - return if config.nil? + def make_header(config, use_agent = nil) + return if config.nil? || config == OPT_OUT header = new(config) [HEADER_NAME, header.value] diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index a637d19d..a71ab14b 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -201,6 +201,7 @@ module ClassMethods # Returns a default policy if no configuration is provided, or a # header name and value based on the config. def make_header(config, user_agent) + return if config.nil? || config == OPT_OUT header = new(config, user_agent) [header.name, header.value] end diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb index 7734f932..6c40256f 100644 --- a/lib/secure_headers/headers/public_key_pins.rb +++ b/lib/secure_headers/headers/public_key_pins.rb @@ -12,8 +12,8 @@ class << self # Public: make an hpkp header name, value pair # # Returns nil if not configured, returns header name and value if configured. - def make_header(config) - return if config.nil? + def make_header(config, user_agent = nil) + return if config.nil? || config == OPT_OUT header = new(config) [header.name, header.value] end diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index e91e4331..e33ec211 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -21,7 +21,8 @@ class << self # # Returns a default header if no configuration is provided, or a # header name and value based on the config. - def make_header(config = nil) + def make_header(config = nil, user_agent = nil) + return if config == OPT_OUT config ||= DEFAULT_VALUE [HEADER_NAME, Array(config).join(", ")] end diff --git a/lib/secure_headers/headers/strict_transport_security.rb b/lib/secure_headers/headers/strict_transport_security.rb index ccb58030..8ccd194a 100644 --- a/lib/secure_headers/headers/strict_transport_security.rb +++ b/lib/secure_headers/headers/strict_transport_security.rb @@ -15,7 +15,8 @@ class << self # # Returns a default header if no configuration is provided, or a # header name and value based on the config. - def make_header(config = nil) + def make_header(config = nil, user_agent = nil) + return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index bc30ba65..81ee8175 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -12,7 +12,8 @@ class << self # # Returns a default header if no configuration is provided, or a # header name and value based on the config. - def make_header(config = nil) + def make_header(config = nil, user_agent = nil) + return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end diff --git a/lib/secure_headers/headers/x_download_options.rb b/lib/secure_headers/headers/x_download_options.rb index 076a682e..5c0307f0 100644 --- a/lib/secure_headers/headers/x_download_options.rb +++ b/lib/secure_headers/headers/x_download_options.rb @@ -11,7 +11,8 @@ class << self # # Returns a default header if no configuration is provided, or a # header name and value based on the config. - def make_header(config = nil) + def make_header(config = nil, user_agent = nil) + return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end diff --git a/lib/secure_headers/headers/x_frame_options.rb b/lib/secure_headers/headers/x_frame_options.rb index 863e4e56..f99c9966 100644 --- a/lib/secure_headers/headers/x_frame_options.rb +++ b/lib/secure_headers/headers/x_frame_options.rb @@ -16,7 +16,7 @@ class << self # # Returns a default header if no configuration is provided, or a # header name and value based on the config. - def make_header(config = nil) + def make_header(config = nil, user_agent = nil) return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end diff --git a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb index 73e099ae..5b8e5e53 100644 --- a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +++ b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb @@ -12,7 +12,8 @@ class << self # # Returns a default header if no configuration is provided, or a # header name and value based on the config. - def make_header(config = nil) + def make_header(config = nil, user_agent = nil) + return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index edeefda1..34a961b5 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -12,7 +12,8 @@ class << self # # Returns a default header if no configuration is provided, or a # header name and value based on the config. - def make_header(config = nil) + def make_header(config = nil, user_agent = nil) + return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index ae8c80ac..992506e5 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -12,76 +12,16 @@ module SecureHeaders expect(Configuration.get(Configuration::DEFAULT_CONFIG)).to_not be_nil end - it "has an 'noop' config" do - expect(Configuration.get(Configuration::NOOP_CONFIGURATION)).to_not be_nil + it "has an 'noop' override" do + expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil end - it "precomputes headers upon creation" do - default_config = Configuration.get(Configuration::DEFAULT_CONFIG) - header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash| - header_name, header_value = if key == :csp - value["Chrome"] - else - value - end - - hash[header_name] = header_value - end - expect_default_values(header_hash) - end - - it "copies config values when duping" do - Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do - # do nothing, just copy it - end - - config = Configuration.get(:test_override) - noop = Configuration.get(Configuration::NOOP_CONFIGURATION) - [:csp, :csp_report_only, :cookies].each do |key| - expect(config.send(key)).to eq(noop.send(key)) - end - end - - it "regenerates cached headers when building an override" do - Configuration.override(:test_override) do |config| - config.x_content_type_options = OPT_OUT - end - - expect(Configuration.get.cached_headers).to_not eq(Configuration.get(:test_override).cached_headers) - end - - it "stores an override of the global config" do + it "stores an override" do Configuration.override(:test_override) do |config| config.x_frame_options = "DENY" end - expect(Configuration.get(:test_override)).to_not be_nil - end - - it "deep dup's config values when overriding so the original cannot be modified" do - Configuration.override(:override) do |config| - config.csp[:default_src] << "'self'" - end - - default = Configuration.get - override = Configuration.get(:override) - - expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src)) - end - - it "allows you to override an override" do - Configuration.override(:override) do |config| - config.csp = { default_src: %w('self'), script_src: %w('self')} - end - - Configuration.override(:second_override, :override) do |config| - config.csp = config.csp.merge(script_src: %w(example.org)) - end - - original_override = Configuration.get(:override) - expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self')) - override_config = Configuration.get(:second_override) - expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org)) + expect(Configuration.overrides(:test_override)).to_not be_nil end it "deprecates the secure_cookies configuration" do diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index cc49cb5a..c4b793ad 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -50,7 +50,6 @@ module SecureHeaders end request = Rack::Request.new({}) SecureHeaders.use_secure_headers_override(request, "my_custom_config") - expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config")) _, env = middleware.call request.env expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org") end @@ -66,7 +65,7 @@ module SecureHeaders end it "allows opting out of cookie protection with OPT_OUT alone" do - Configuration.default { |config| config.cookies = OPT_OUT} + Configuration.default { |config| config.cookies = OPT_OUT } # do NOT make this request https. non-https requests modify a config, # causing an exception when operating on OPT_OUT. This ensures we don't diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 54387dbf..cf5c67f5 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -67,7 +67,7 @@ def content_tag(type, content = nil, options = nil, &block) end if options.is_a?(Hash) - options = options.map {|k, v| " #{k}=#{v}"} + options = options.map { |k, v| " #{k}=#{v}" } end "<#{type}#{options}>#{content}" end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 75a40b7b..4c038c52 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -60,6 +60,24 @@ module SecureHeaders expect(hash["X-Frame-Options"]).to be_nil end + it "Overrides the current default config if default config changes during request" do + Configuration.default do |config| + config.x_frame_options = OPT_OUT + end + + # Dynamically update the default config for this request + SecureHeaders.override_x_frame_options(request, "DENY") + + Configuration.override(:dynamic_override) do |config| + config.x_content_type_options = "nosniff" + end + + SecureHeaders.use_secure_headers_override(request, :dynamic_override) + hash = SecureHeaders.header_hash_for(request) + expect(hash["X-Content-Type-Options"]).to eq("nosniff") + expect(hash["X-Frame-Options"]).to eq("DENY") + end + it "allows you to opt out entirely" do # configure the disabled-by-default headers to ensure they also do not get set Configuration.default do |config| @@ -78,9 +96,6 @@ module SecureHeaders end SecureHeaders.opt_out_of_all_protection(request) hash = SecureHeaders.header_hash_for(request) - ALL_HEADER_CLASSES.each do |klass| - expect(hash[klass::CONFIG_KEY]).to be_nil - end expect(hash.count).to eq(0) end From 7c804daa2158c9954ba03c7c272867ccc65627ee Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Fri, 19 Jan 2018 17:06:16 -0700 Subject: [PATCH 461/636] Make merge and merge! consistent --- lib/secure_headers/headers/content_security_policy_config.rb | 5 ++++- spec/lib/secure_headers_spec.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 254bf9a5..8d4cb193 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -54,7 +54,10 @@ def directive_value(directive) end def merge(new_hash) - ContentSecurityPolicy.combine_policies(self.to_h, new_hash) + new_config = self.dup + puts new_config.inspect + new_config.send(:from_hash, new_hash) + new_config end def merge!(new_hash) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 4c038c52..70f8ab5c 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -391,7 +391,7 @@ module SecureHeaders hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' foo.com") + expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src foo.com") end it "allows you to opt-out of enforced CSP" do From f217d1958c56e37c2d879fc0aa373415f2ab18f2 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Fri, 19 Jan 2018 17:15:03 -0700 Subject: [PATCH 462/636] Remove CONFIG_KEY since it isn't used anywhere anymore --- lib/secure_headers/headers/clear_site_data.rb | 2 -- .../headers/content_security_policy_config.rb | 1 - .../headers/expect_certificate_transparency.rb | 1 - lib/secure_headers/headers/public_key_pins.rb | 1 - lib/secure_headers/headers/referrer_policy.rb | 1 - lib/secure_headers/headers/strict_transport_security.rb | 1 - lib/secure_headers/headers/x_content_type_options.rb | 1 - lib/secure_headers/headers/x_download_options.rb | 1 - lib/secure_headers/headers/x_frame_options.rb | 1 - .../headers/x_permitted_cross_domain_policies.rb | 1 - lib/secure_headers/headers/x_xss_protection.rb | 3 +-- spec/lib/secure_headers_spec.rb | 8 ++++---- 12 files changed, 5 insertions(+), 17 deletions(-) diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index 6e70d985..563ed840 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -11,8 +11,6 @@ class ClearSiteData EXECUTION_CONTEXTS = "executionContexts".freeze ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS] - CONFIG_KEY = :clear_site_data - class << self # Public: make an Clear-Site-Data header name, value pair # diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 8d4cb193..b568aa89 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -112,7 +112,6 @@ def write_attribute(attr, value) class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicyConfig - CONFIG_KEY = :csp HEADER_NAME = "Content-Security-Policy".freeze ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES diff --git a/lib/secure_headers/headers/expect_certificate_transparency.rb b/lib/secure_headers/headers/expect_certificate_transparency.rb index c40acb58..b65cbd09 100644 --- a/lib/secure_headers/headers/expect_certificate_transparency.rb +++ b/lib/secure_headers/headers/expect_certificate_transparency.rb @@ -4,7 +4,6 @@ class ExpectCertificateTransparencyConfigError < StandardError; end class ExpectCertificateTransparency HEADER_NAME = "Expect-CT".freeze - CONFIG_KEY = :expect_certificate_transparency INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb index 6c40256f..f0850344 100644 --- a/lib/secure_headers/headers/public_key_pins.rb +++ b/lib/secure_headers/headers/public_key_pins.rb @@ -5,7 +5,6 @@ class PublicKeyPins HEADER_NAME = "Public-Key-Pins".freeze REPORT_ONLY = "Public-Key-Pins-Report-Only".freeze HASH_ALGORITHMS = [:sha256].freeze - CONFIG_KEY = :hpkp class << self diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index e33ec211..c18188f8 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -14,7 +14,6 @@ class ReferrerPolicy origin-when-cross-origin unsafe-url ) - CONFIG_KEY = :referrer_policy class << self # Public: generate an Referrer Policy header. diff --git a/lib/secure_headers/headers/strict_transport_security.rb b/lib/secure_headers/headers/strict_transport_security.rb index 8ccd194a..36df032c 100644 --- a/lib/secure_headers/headers/strict_transport_security.rb +++ b/lib/secure_headers/headers/strict_transport_security.rb @@ -8,7 +8,6 @@ class StrictTransportSecurity DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}" - CONFIG_KEY = :hsts class << self # Public: generate an hsts header name, value pair. diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index 81ee8175..cf4371f5 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -5,7 +5,6 @@ class XContentTypeOptionsConfigError < StandardError; end class XContentTypeOptions HEADER_NAME = "X-Content-Type-Options".freeze DEFAULT_VALUE = "nosniff" - CONFIG_KEY = :x_content_type_options class << self # Public: generate an X-Content-Type-Options header. diff --git a/lib/secure_headers/headers/x_download_options.rb b/lib/secure_headers/headers/x_download_options.rb index 5c0307f0..db327dca 100644 --- a/lib/secure_headers/headers/x_download_options.rb +++ b/lib/secure_headers/headers/x_download_options.rb @@ -4,7 +4,6 @@ class XDOConfigError < StandardError; end class XDownloadOptions HEADER_NAME = "X-Download-Options".freeze DEFAULT_VALUE = "noopen" - CONFIG_KEY = :x_download_options class << self # Public: generate an X-Download-Options header. diff --git a/lib/secure_headers/headers/x_frame_options.rb b/lib/secure_headers/headers/x_frame_options.rb index f99c9966..508ae364 100644 --- a/lib/secure_headers/headers/x_frame_options.rb +++ b/lib/secure_headers/headers/x_frame_options.rb @@ -3,7 +3,6 @@ module SecureHeaders class XFOConfigError < StandardError; end class XFrameOptions HEADER_NAME = "X-Frame-Options".freeze - CONFIG_KEY = :x_frame_options SAMEORIGIN = "sameorigin" DENY = "deny" ALLOW_FROM = "allow-from" diff --git a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb index 5b8e5e53..416c9f01 100644 --- a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +++ b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb @@ -5,7 +5,6 @@ class XPermittedCrossDomainPolicies HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze DEFAULT_VALUE = "none" VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) - CONFIG_KEY = :x_permitted_cross_domain_policies class << self # Public: generate an X-Permitted-Cross-Domain-Policies header. diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index 34a961b5..27a99c02 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -4,8 +4,7 @@ class XXssProtectionConfigError < StandardError; end class XXssProtection HEADER_NAME = "X-XSS-Protection".freeze DEFAULT_VALUE = "1; mode=block" - VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i - CONFIG_KEY = :x_xss_protection + VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/ class << self # Public: generate an X-Xss-Protection header. diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 70f8ab5c..1b439604 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -17,7 +17,7 @@ module SecureHeaders it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do expect do - SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, :csp) end.to raise_error(Configuration::NotYetConfiguredError) end @@ -34,9 +34,9 @@ module SecureHeaders config.csp = { default_src: %w('self'), script_src: %w('self')} config.csp_report_only = config.csp end - SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY) - SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY) - SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY) + SecureHeaders.opt_out_of_header(request, :csp) + SecureHeaders.opt_out_of_header(request, :csp_report_only) + SecureHeaders.opt_out_of_header(request, :x_content_type_options) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy-Report-Only"]).to be_nil expect(hash["Content-Security-Policy"]).to be_nil From 830b4b23a39a0ccceb1c1532c501ecedb3b0980c Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Fri, 19 Jan 2018 17:20:29 -0700 Subject: [PATCH 463/636] Add all headers to expect_default_values --- spec/spec_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0c00eb92..5a8dffe1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,6 +26,7 @@ def expect_default_values(hash) expect(hash[SecureHeaders::ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'") + expect(hash[SecureHeaders::ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) @@ -34,6 +35,9 @@ def expect_default_values(hash) expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE) expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil + expect(hash[SecureHeaders::ClearSiteData::HEADER_NAME]).to be_nil + expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil + expect(hash[SecureHeaders::PublicKeyPins::HEADER_NAME]).to be_nil end module SecureHeaders From cc7b2b50ec572750069fbfbca14a510f1f584b0e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 19 Jan 2018 15:07:04 -1000 Subject: [PATCH 464/636] unconditionally send nonces and unsafe-inline when working with nonces --- .../headers/content_security_policy.rb | 10 +----- .../headers/policy_management.rb | 9 ------ .../headers/content_security_policy_spec.rb | 24 +++++++------- spec/lib/secure_headers_spec.rb | 31 +------------------ 4 files changed, 14 insertions(+), 60 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 42751817..67bd18d5 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -212,11 +212,7 @@ def populate_nonces(directive, source_list) # unsafe-inline, this is more concise. def append_nonce(source_list, nonce) if nonce - if nonces_supported? - source_list << "'nonce-#{nonce}'" - else - source_list << UNSAFE_INLINE - end + source_list.push(*["'nonce-#{nonce}'", UNSAFE_INLINE]) end source_list @@ -256,10 +252,6 @@ def supported_directives end end - def nonces_supported? - @nonces_supported ||= self.class.nonces_supported?(@parsed_ua) - end - def symbol_to_hyphen_case(sym) sym.to_s.tr("_", "-") end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index a637d19d..edc8d904 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -227,15 +227,6 @@ def validate_config!(config) end end - # Public: check if a user agent supports CSP nonces - # - # user_agent - a String or a UserAgent object - def nonces_supported?(user_agent) - user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String) - MODERN_BROWSERS.include?(user_agent.browser) || - user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10 - end - # Public: combine the values from two different configs. # # original - the main config diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 2f190607..f2462711 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -124,7 +124,7 @@ module SecureHeaders it "supports strict-dynamic" do csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome]) - expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'") + expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'") end context "browser sniffing" do @@ -143,44 +143,44 @@ module SecureHeaders it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com") end it "does not filter any directives for Opera" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end - it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do + it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for Edge" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge]) - expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end - it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do + it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for safari" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) - expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end - it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do + it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, and hash sources for safari 10 and higher" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end it "falls back to standard Firefox defaults when the useragent version is not present" do ua = USER_AGENTS[:firefox].dup allow(ua).to receive(:version).and_return(nil) policy = ContentSecurityPolicy.new(complex_opts, ua) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") + expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 75a40b7b..18372032 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -265,21 +265,6 @@ module SecureHeaders expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'") end - it "does not append a nonce when the browser does not support it" do - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - script_src: %w(mycdn.com 'unsafe-inline'), - style_src: %w('self') - } - end - - safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5])) - SecureHeaders.content_security_policy_script_nonce(safari_request) - hash = SecureHeaders.header_hash_for(safari_request) - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'") - end - it "appends a nonce to the script-src when used" do Configuration.default do |config| config.csp = { @@ -297,21 +282,7 @@ module SecureHeaders SecureHeaders.content_security_policy_script_nonce(chrome_request) hash = SecureHeaders.header_hash_for(chrome_request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") - end - - it "uses a nonce for safari 10+" do - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - script_src: %w(mycdn.com) - } - end - - safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10])) - nonce = SecureHeaders.content_security_policy_script_nonce(safari_request) - hash = SecureHeaders.header_hash_for(safari_request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'") + expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}' 'unsafe-inline'; style-src 'self'") end it "does not support the deprecated `report_only: true` format" do From 584c3511ada2577bcd41cc6e8e5ee1802f35466e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 19 Jan 2018 15:15:10 -1000 Subject: [PATCH 465/636] remove unused constant (and shade) --- lib/secure_headers/headers/policy_management.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index edc8d904..d31b5f5e 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -5,7 +5,6 @@ def self.included(base) base.extend(ClassMethods) end - MODERN_BROWSERS = %w(Chrome Opera Firefox) DEFAULT_CONFIG = { default_src: %w(https:), img_src: %w(https: data: 'self'), From 6ac2f46428bc5e453653bad96e905d040e17ac21 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Mon, 22 Jan 2018 11:11:14 -0700 Subject: [PATCH 466/636] Remove all notions of a set of stored configurations The current code stored named configs in class instance hash. Since the only config that should ever exist is now the default config (all other cases are handled by dynamic overrides and not configs), we remove all vestiages of the configuration hash lookup. --- lib/secure_headers.rb | 3 +- lib/secure_headers/configuration.rb | 118 +++++++----------- .../headers/content_security_policy_config.rb | 2 - .../headers/policy_management.rb | 10 ++ spec/lib/secure_headers/configuration_spec.rb | 2 +- .../headers/policy_management_spec.rb | 14 ++- spec/lib/secure_headers_spec.rb | 4 +- spec/spec_helper.rb | 6 +- 8 files changed, 77 insertions(+), 82 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 3620e184..873ee7fd 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -148,6 +148,7 @@ def opt_out_of_all_protection(request) def header_hash_for(request) prevent_dup = true config = config_for(request, prevent_dup) + config.validate_config! user_agent = UserAgent.parse(request.user_agent) headers = config.generate_headers(user_agent) @@ -198,7 +199,7 @@ def content_security_policy_style_nonce(request) # Falls back to the global config def config_for(request, prevent_dup = false) config = request.env[SECURE_HEADERS_CONFIG] || - Configuration.get(Configuration::DEFAULT_CONFIG) + Configuration.get # Global configs are frozen, per-request configs are not. When we're not diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index deda74bd..44219a85 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -14,17 +14,25 @@ class << self # # Returns the newly created config. def default(&block) - config = new(&block) - add_configuration(DEFAULT_CONFIG, config) + # Define a built-in override that clears all configuration options and + # results in no security headers being set. override(NOOP_OVERRIDE) do |config| CONFIG_ATTRIBUTES.each do |attr| config.instance_variable_set("@#{attr}", OPT_OUT) end end - config + + new_config = new(&block).freeze + new_config.validate_config! + @default_config = new_config end alias_method :configure, :default + def get + raise NotYetConfiguredError, "Default policy not yet supplied" unless @default_config + @default_config + end + # Public: create a named configuration that overrides the default config. # # name - use an idenfier for the override config. @@ -34,6 +42,7 @@ def default(&block) # Returns: the newly created config def override(name, &block) @overrides ||= {} + raise "Provide a configuration block" unless block_given? @overrides[name] = block end @@ -42,23 +51,12 @@ def overrides(name) @overrides[name] end - # Public: retrieve a global configuration object - # - # Returns the configuration with a given name or raises a - # NotYetConfiguredError if `default` has not been called. - def get(name = DEFAULT_CONFIG) - if @configurations.nil? - raise NotYetConfiguredError, "Default policy not yet supplied" - end - @configurations[name] - end - def named_appends(name) @appends ||= {} @appends[name] end - def named_append(name, target = nil, &block) + def named_append(name, &block) @appends ||= {} raise "Provide a configuration block" unless block_given? @appends[name] = block @@ -66,22 +64,6 @@ def named_append(name, target = nil, &block) private - # Private: add a valid configuration to the global set of named configs. - # - # config - the config to store - # name - the lookup value for this config - # - # Raises errors if the config is invalid or if a config named `name` - # already exists. - # - # Returns the config, if valid - def add_configuration(name, config) - config.validate_config! - @configurations ||= {} - config.freeze - @configurations[name] = config - end - # Public: perform a basic deep dup. The shallow copy provided by dup/clone # can lead to modifying parent objects. def deep_copy(config) @@ -106,11 +88,7 @@ def deep_copy_if_hash(value) end end - NON_HEADER_ATTRIBUTES = [ - :cookies, :hpkp_report_host - ].freeze - - HEADER_ATTRIBUTES_TO_HEADER_CLASSES = { + CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = { hsts: StrictTransportSecurity, x_frame_options: XFrameOptions, x_content_type_options: XContentTypeOptions, @@ -123,11 +101,18 @@ def deep_copy_if_hash(value) csp: ContentSecurityPolicy, csp_report_only: ContentSecurityPolicy, hpkp: PublicKeyPins, + cookies: Cookie, }.freeze - CONFIG_ATTRIBUTES = (HEADER_ATTRIBUTES_TO_HEADER_CLASSES.keys + NON_HEADER_ATTRIBUTES).freeze + CONFIG_ATTRIBUTES = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys.freeze - attr_accessor(*CONFIG_ATTRIBUTES) + # The list of attributes that must respond to a `validate_config!` method + VALIDATABLE_ATTRIBUTES = CONFIG_ATTRIBUTES + + # The list of attributes that must respond to a `make_header` method + HEADERABLE_ATTRIBUTES = (CONFIG_ATTRIBUTES - [:cookies]).freeze + + attr_accessor(*CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys) @script_hashes = nil @style_hashes = nil @@ -144,7 +129,6 @@ def initialize(&block) @clear_site_data = nil @csp = nil @csp_report_only = nil - @hpkp_report_host = nil @hpkp = nil @hsts = nil @x_content_type_options = nil @@ -185,7 +169,8 @@ def dup def generate_headers(user_agent) headers = {} - HEADER_ATTRIBUTES_TO_HEADER_CLASSES.each do |attr, klass| + HEADERABLE_ATTRIBUTES.each do |attr| + klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr] header_name, value = klass.make_header(instance_variable_get("@#{attr}"), user_agent) if header_name && value headers[header_name] = value @@ -208,10 +193,10 @@ def update_x_frame_options(value) # # Returns nothing def validate_config! - HEADER_ATTRIBUTES_TO_HEADER_CLASSES.each do |attr, klass| + VALIDATABLE_ATTRIBUTES.each do |attr| + klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr] klass.validate_config!(instance_variable_get("@#{attr}")) end - Cookie.validate_config!(@cookies) end def secure_cookies=(secure_cookies) @@ -219,15 +204,15 @@ def secure_cookies=(secure_cookies) end def csp=(new_csp) - if new_csp.respond_to?(:opt_out?) - @csp = new_csp.dup + case new_csp + when OPT_OUT + @csp = new_csp + when ContentSecurityPolicyConfig + @csp = new_csp + when Hash + @csp = ContentSecurityPolicyConfig.new(new_csp) else - if new_csp[:report_only] - # invalid configuration implies that CSPRO should be set, CSP should not - so opt out - raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" - else - @csp = ContentSecurityPolicyConfig.new(new_csp) - end + raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash" end end @@ -238,18 +223,17 @@ def csp=(new_csp) # configuring csp_report_only, the code will assume you mean to only use # report-only mode and you will be opted-out of enforce mode. def csp_report_only=(new_csp) - @csp_report_only = begin - if new_csp.is_a?(ContentSecurityPolicyConfig) - new_csp.make_report_only - elsif new_csp.respond_to?(:opt_out?) - new_csp.dup - else - if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise - raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp=" - else - ContentSecurityPolicyReportOnlyConfig.new(new_csp) - end - end + case new_csp + when OPT_OUT + @csp_report_only = new_csp + when ContentSecurityPolicyReportOnlyConfig + @csp_report_only = new_csp.dup + when ContentSecurityPolicyConfig + @csp_report_only = new_csp.make_report_only + when Hash + @csp_report_only = ContentSecurityPolicyReportOnlyConfig.new(new_csp) + else + raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash" end end @@ -257,15 +241,5 @@ def hpkp_report_host return nil unless @hpkp && hpkp != OPT_OUT && @hpkp[:report_uri] URI.parse(@hpkp[:report_uri]).host end - - protected - - def cookies=(cookies) - @cookies = cookies - end - - def hpkp=(hpkp) - @hpkp = self.class.send(:deep_copy_if_hash, hpkp) - end end end diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index b568aa89..9acf6212 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -55,7 +55,6 @@ def directive_value(directive) def merge(new_hash) new_config = self.dup - puts new_config.inspect new_config.send(:from_hash, new_hash) new_config end @@ -141,7 +140,6 @@ def make_report_only end class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig - CONFIG_KEY = :csp_report_only HEADER_NAME = "Content-Security-Policy-Report-Only".freeze def report_only? diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index a71ab14b..7a19c174 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -216,12 +216,22 @@ def validate_config!(config) if config.directive_value(:script_src).nil? raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override") end + if !config.report_only? && config.directive_value(:report_only) + raise ContentSecurityPolicyConfigError.new("Only the csp_report_only config should sset :report_only to true") + end + + if config.report_only? && config.directive_value(:report_only) == false + raise ContentSecurityPolicyConfigError.new("csp_report_only config must have :report_only set to true") + end ContentSecurityPolicyConfig.attrs.each do |key| value = config.directive_value(key) next unless value + if META_CONFIGS.include?(key) raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil? + elsif NONCES.include?(key) + raise ContentSecurityPolicyConfigError.new("#{key} must be a non-nil value") if value.nil? else validate_directive!(key, value) end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 992506e5..ecc02a4b 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -9,7 +9,7 @@ module SecureHeaders end it "has a default config" do - expect(Configuration.get(Configuration::DEFAULT_CONFIG)).to_not be_nil + expect(Configuration.default).to_not be_nil end it "has an 'noop' override" do diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 24d35cbc..d5971447 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -18,7 +18,7 @@ module SecureHeaders # (pulled from README) config = { # "meta" values. these will shape the header, but the values are not included in the header. - report_only: true, # default: false + report_only: false, preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives @@ -142,6 +142,18 @@ module SecureHeaders ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"]))) end.to_not raise_error end + + it "doesn't allow report_only to be set in a non-report-only config" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: true))) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "allows report_only to be set in a report-only config" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyReportOnlyConfig.new(default_opts.merge(report_only: true))) + end.to_not raise_error + end end describe "#combine_policies" do diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 1b439604..de61c0e4 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -131,7 +131,7 @@ module SecureHeaders firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox])) # append an unsupported directive - SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)}) + SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(application/pdf)}) # append a supported directive SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')}) @@ -337,7 +337,7 @@ module SecureHeaders report_only: true } end - }.to raise_error(ArgumentError) + }.to raise_error(ContentSecurityPolicyConfigError) end it "Raises an error if csp_report_only is used with `report_only: false`" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5a8dffe1..00b2ac3d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -43,13 +43,13 @@ def expect_default_values(hash) module SecureHeaders class Configuration class << self - def clear_configurations - @configurations = nil + def clear_default_config + @default_config = nil end end end end def reset_config - SecureHeaders::Configuration.clear_configurations + SecureHeaders::Configuration.clear_default_config end From 4fb96620d9dac8f5f8b1624f68f25ce00244ee4d Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Mon, 22 Jan 2018 11:20:30 -0700 Subject: [PATCH 467/636] Move override application logic to configuration class While the normal expected use case for an override is to apply it to a request, there are uses cases where we might want to apply an override to a non-request context. For example, we have a script that runs outside of the main rails app to generate some static configs we copy/paste into some static files. It would look something like this: ``` _, static_csp_policy = SecureHeaders::CSP.make_header(SecureHeaders::Configuration.get.apply_overrides(:static_file_policy).csp, nil) ... %q( Date: Tue, 30 Jan 2018 09:37:08 -0700 Subject: [PATCH 468/636] Make user_agent optional everywhere end also handle an explicit nil in ContentSecurityPolicy --- lib/secure_headers/headers/content_security_policy.rb | 1 + lib/secure_headers/headers/policy_management.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 42751817..3db297f5 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -13,6 +13,7 @@ class ContentSecurityPolicy FALLBACK_VERSION = ::UserAgent::Version.new("0") def initialize(config = nil, user_agent = OTHER) + user_agent ||= OTHER @config = if config.is_a?(Hash) if config[:report_only] ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 7a19c174..204983a6 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -200,7 +200,7 @@ module ClassMethods # # Returns a default policy if no configuration is provided, or a # header name and value based on the config. - def make_header(config, user_agent) + def make_header(config, user_agent = nil) return if config.nil? || config == OPT_OUT header = new(config, user_agent) [header.name, header.value] From 06fce94c4c80edeef5ddca37ac785aae0964f55d Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 30 Jan 2018 10:26:00 -0700 Subject: [PATCH 469/636] Replace #get with #default and don't require explicit initial configuration anymore --- lib/secure_headers.rb | 4 ++-- lib/secure_headers/configuration.rb | 22 ++++++++++++------- spec/lib/secure_headers/configuration_spec.rb | 6 ++--- .../headers/policy_management_spec.rb | 16 +++++++++----- spec/lib/secure_headers/middleware_spec.rb | 1 - spec/lib/secure_headers/view_helpers_spec.rb | 1 + spec/lib/secure_headers_spec.rb | 18 +++++---------- spec/spec_helper.rb | 2 +- 8 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 45cf4686..c053114a 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -166,7 +166,7 @@ def header_hash_for(request) # name - the name of the previously configured override. def use_secure_headers_override(request, name) config = config_for(request) - config.apply_override(name) + config.override(name) override_secure_headers_request_config(request, config) end @@ -195,7 +195,7 @@ def content_security_policy_style_nonce(request) # Falls back to the global config def config_for(request, prevent_dup = false) config = request.env[SECURE_HEADERS_CONFIG] || - Configuration.get + Configuration.default # Global configs are frozen, per-request configs are not. When we're not diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 070f1587..ad585b1b 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -5,7 +5,7 @@ module SecureHeaders class Configuration DEFAULT_CONFIG = :default NOOP_OVERRIDE = "secure_headers_noop_override" - class NotYetConfiguredError < StandardError; end + class AlreadyConfiguredError < StandardError; end class IllegalPolicyModificationError < StandardError; end class << self # Public: Set the global default configuration. @@ -14,6 +14,18 @@ class << self # # Returns the newly created config. def default(&block) + if defined?(@default_config) && !block_given? + return @default_config + else + configure(&block) + end + end + + def configure(&block) + if defined?(@default_config) + raise AlreadyConfiguredError, "Policy already configured" + end + # Define a built-in override that clears all configuration options and # results in no security headers being set. override(NOOP_OVERRIDE) do |config| @@ -26,12 +38,6 @@ def default(&block) new_config.validate_config! @default_config = new_config end - alias_method :configure, :default - - def get - raise NotYetConfiguredError, "Default policy not yet supplied" unless @default_config - @default_config - end # Public: create a named configuration that overrides the default config. # @@ -170,7 +176,7 @@ def dup # Public: Apply a named override to the current config # # Returns self - def apply_override(name) + def override(name = nil, &block) if override = self.class.overrides(name) instance_eval(&override) else diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index ecc02a4b..c7e05843 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -5,7 +5,6 @@ module SecureHeaders describe Configuration do before(:each) do reset_config - Configuration.default end it "has a default config" do @@ -13,6 +12,7 @@ module SecureHeaders end it "has an 'noop' override" do + Configuration.default expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil end @@ -41,7 +41,7 @@ module SecureHeaders config.cookies = OPT_OUT end - config = Configuration.get + config = Configuration.default expect(config.cookies).to eq(OPT_OUT) end @@ -50,7 +50,7 @@ module SecureHeaders config.cookies = {httponly: true, secure: true, samesite: {lax: false}} end - config = Configuration.get + config = Configuration.default expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}}) end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index d5971447..6127d494 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -3,6 +3,10 @@ module SecureHeaders describe PolicyManagement do + before(:each) do + reset_config + end + let (:default_opts) do { default_src: %w(https:), @@ -164,7 +168,7 @@ module SecureHeaders script_src: %w('self'), } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, style_src: %w(anothercdn.com)) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, style_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com") @@ -179,7 +183,7 @@ module SecureHeaders }.freeze end report_uri = "https://report-uri.io/asdf" - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri]) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, report_uri: [report_uri]) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.value).to include("report-uri #{report_uri}") end @@ -195,7 +199,7 @@ module SecureHeaders non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, non_default_source_additions) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, non_default_source_additions) ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) @@ -210,7 +214,7 @@ module SecureHeaders report_only: false } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_only: true) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, report_only: true) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) end @@ -223,7 +227,7 @@ module SecureHeaders block_all_mixed_content: false } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'") end @@ -233,7 +237,7 @@ module SecureHeaders config.csp = OPT_OUT end expect do - ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) + ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, script_src: %w(anothercdn.com)) end.to raise_error(ContentSecurityPolicyConfigError) end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index c4b793ad..f669708a 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -11,7 +11,6 @@ module SecureHeaders before(:each) do reset_config - Configuration.default end it "warns if the hpkp report-uri host is the same as the current host" do diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index cf5c67f5..67cc0d22 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -105,6 +105,7 @@ module SecureHeaders let(:filename) { "app/views/asdfs/index.html.erb" } before(:all) do + reset_config Configuration.default do |config| config.csp = { default_src: %w('self'), diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index de61c0e4..e3717dce 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -9,18 +9,6 @@ module SecureHeaders let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") } - it "raises a NotYetConfiguredError if default has not been set" do - expect do - SecureHeaders.header_hash_for(request) - end.to raise_error(Configuration::NotYetConfiguredError) - end - - it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do - expect do - SecureHeaders.opt_out_of_header(request, :csp) - end.to raise_error(Configuration::NotYetConfiguredError) - end - it "raises and ArgumentError when referencing an override that has not been set" do expect do Configuration.default @@ -364,6 +352,7 @@ module SecureHeaders end it "sets identical values when the configs are the same" do + reset_config Configuration.default do |config| config.csp = { default_src: %w('self'), @@ -381,6 +370,7 @@ module SecureHeaders end it "sets different headers when the configs are different" do + reset_config Configuration.default do |config| config.csp = { default_src: %w('self'), @@ -395,6 +385,7 @@ module SecureHeaders end it "allows you to opt-out of enforced CSP" do + reset_config Configuration.default do |config| config.csp = SecureHeaders::OPT_OUT config.csp_report_only = { @@ -452,6 +443,7 @@ module SecureHeaders context "when inferring which config to modify" do it "updates the enforced header when configured" do + reset_config Configuration.default do |config| config.csp = { default_src: %w('self'), @@ -466,6 +458,7 @@ module SecureHeaders end it "updates the report only header when configured" do + reset_config Configuration.default do |config| config.csp = OPT_OUT config.csp_report_only = { @@ -481,6 +474,7 @@ module SecureHeaders end it "updates both headers if both are configured" do + reset_config Configuration.default do |config| config.csp = { default_src: %w(enforced.com), diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00b2ac3d..96f06264 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,7 +44,7 @@ module SecureHeaders class Configuration class << self def clear_default_config - @default_config = nil + remove_instance_variable(:@default_config) if defined?(@default_config) end end end From 5cc33e55f1371b5fa08fe3d358de8836b29faa92 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 30 Jan 2018 10:30:43 -0700 Subject: [PATCH 470/636] Update reame to use new config idiom --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d79d386d..d2ce7f87 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,14 @@ use SecureHeaders::Middleware ## Configuration -If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. +If you do not supply a configuration, a default (which is fairly locked down) will be used. All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. **Word of caution:** The following is not a default configuration per se. It serves as a sample implementation of the configuration. You should read more about these headers and determine what is appropriate for your requirements. ```ruby -SecureHeaders::Configuration.default do |config| +SecureHeaders::Configuration.configure do |config| config.cookies = { secure: true, # mark all cookies as "Secure" httponly: true, # mark all cookies as "HttpOnly" From 68b2d2fa234631ff3e186ff33e330f0518954547 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 30 Jan 2018 10:43:09 -0700 Subject: [PATCH 471/636] Use the original idiom --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2ce7f87..02421bcb 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` **Word of caution:** The following is not a default configuration per se. It serves as a sample implementation of the configuration. You should read more about these headers and determine what is appropriate for your requirements. ```ruby -SecureHeaders::Configuration.configure do |config| +SecureHeaders::Configuration.default do |config| config.cookies = { secure: true, # mark all cookies as "Secure" httponly: true, # mark all cookies as "HttpOnly" From b405ba37cfde1a2d935e1ae9592f2a0f14f96770 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 30 Jan 2018 11:25:50 -0700 Subject: [PATCH 472/636] Revert back to original readme contents --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02421bcb..d79d386d 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ use SecureHeaders::Middleware ## Configuration -If you do not supply a configuration, a default (which is fairly locked down) will be used. +If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. From 0cce2ec53f77c9dcf7e888a3f5becc9523011f7a Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 30 Jan 2018 11:29:18 -0700 Subject: [PATCH 473/636] Hide default config behind a private class method We don't expect public callers to actually access the default config directly. So, we hide it behind a private class method. There is only one caller in the entire codebase that is not a test. It does make tests a bit uglier, but is probably worth it for the benefits of hiding it from the public API. --- lib/secure_headers.rb | 2 +- lib/secure_headers/configuration.rb | 21 +++++++++++------- spec/lib/secure_headers/configuration_spec.rb | 4 ++-- .../headers/policy_management_spec.rb | 22 ++++++++++++++----- spec/lib/secure_headers/middleware_spec.rb | 8 +++++++ spec/lib/secure_headers_spec.rb | 12 ++++++++++ 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index c053114a..f427e988 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -195,7 +195,7 @@ def content_security_policy_style_nonce(request) # Falls back to the global config def config_for(request, prevent_dup = false) config = request.env[SECURE_HEADERS_CONFIG] || - Configuration.default + Configuration.send(:default_config) # Global configs are frozen, per-request configs are not. When we're not diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index ad585b1b..7982406d 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -6,6 +6,7 @@ class Configuration DEFAULT_CONFIG = :default NOOP_OVERRIDE = "secure_headers_noop_override" class AlreadyConfiguredError < StandardError; end + class NotYetConfiguredError < StandardError; end class IllegalPolicyModificationError < StandardError; end class << self # Public: Set the global default configuration. @@ -14,14 +15,6 @@ class << self # # Returns the newly created config. def default(&block) - if defined?(@default_config) && !block_given? - return @default_config - else - configure(&block) - end - end - - def configure(&block) if defined?(@default_config) raise AlreadyConfiguredError, "Policy already configured" end @@ -38,6 +31,7 @@ def configure(&block) new_config.validate_config! @default_config = new_config end + alias_method :configure, :default # Public: create a named configuration that overrides the default config. # @@ -83,6 +77,17 @@ def deep_copy(config) end end + # Private: Returns the internal default configuration. This should only + # ever be called by internal callers (or tests) that know the semantics + # of ensuring that the default config is never mutated and is dup(ed) + # before it is used in a request. + def default_config + unless defined?(@default_config) + raise NotYetConfiguredError, "Default policy not yet configured" + end + @default_config + end + # Private: convenience method purely DRY things up. The value may not be a # hash (e.g. OPT_OUT, nil) def deep_copy_if_hash(value) diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index c7e05843..ebe0ac1c 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -41,7 +41,7 @@ module SecureHeaders config.cookies = OPT_OUT end - config = Configuration.default + config = Configuration.send(:default_config) expect(config.cookies).to eq(OPT_OUT) end @@ -50,7 +50,7 @@ module SecureHeaders config.cookies = {httponly: true, secure: true, samesite: {lax: false}} end - config = Configuration.default + config = Configuration.send(:default_config) expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}}) end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 6127d494..4e237164 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -5,6 +5,7 @@ module SecureHeaders describe PolicyManagement do before(:each) do reset_config + Configuration.default end let (:default_opts) do @@ -161,6 +162,9 @@ module SecureHeaders end describe "#combine_policies" do + before(:each) do + reset_config + end it "combines the default-src value with the override if the directive was unconfigured" do Configuration.default do |config| config.csp = { @@ -168,7 +172,8 @@ module SecureHeaders script_src: %w('self'), } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, style_src: %w(anothercdn.com)) + default_policy = Configuration.send(:default_config) + combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, style_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com") @@ -183,7 +188,8 @@ module SecureHeaders }.freeze end report_uri = "https://report-uri.io/asdf" - combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, report_uri: [report_uri]) + default_policy = Configuration.send(:default_config) + combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri]) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.value).to include("report-uri #{report_uri}") end @@ -199,7 +205,8 @@ module SecureHeaders non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, non_default_source_additions) + default_policy = Configuration.send(:default_config) + combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, non_default_source_additions) ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) @@ -214,7 +221,8 @@ module SecureHeaders report_only: false } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, report_only: true) + default_policy = Configuration.send(:default_config) + combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) end @@ -227,7 +235,8 @@ module SecureHeaders block_all_mixed_content: false } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, block_all_mixed_content: true) + default_policy = Configuration.send(:default_config) + combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'") end @@ -236,8 +245,9 @@ module SecureHeaders Configuration.default do |config| config.csp = OPT_OUT end + default_policy = Configuration.send(:default_config) expect do - ContentSecurityPolicy.combine_policies(Configuration.default.csp.to_h, script_src: %w(anothercdn.com)) + ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, script_src: %w(anothercdn.com)) end.to raise_error(ContentSecurityPolicyConfigError) end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index f669708a..3725c73e 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -11,10 +11,12 @@ module SecureHeaders before(:each) do reset_config + Configuration.default end it "warns if the hpkp report-uri host is the same as the current host" do report_host = "report-uri.io" + reset_config Configuration.default do |config| config.hpkp = { max_age: 10000000, @@ -54,6 +56,9 @@ module SecureHeaders end context "cookies" do + before(:each) do + reset_config + end context "cookies should be flagged" do it "flags cookies as secure" do Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} } @@ -85,6 +90,9 @@ module SecureHeaders end context "cookies" do + before(:each) do + reset_config + end it "flags cookies from configuration" do Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } } request = Rack::Request.new("HTTPS" => "on") diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index e3717dce..081e8403 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -9,6 +9,18 @@ module SecureHeaders let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") } + it "raises a NotYetConfiguredError if default has not been set" do + expect do + SecureHeaders.header_hash_for(request) + end.to raise_error(Configuration::NotYetConfiguredError) + end + + it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do + expect do + SecureHeaders.opt_out_of_header(request, :csp) + end.to raise_error(Configuration::NotYetConfiguredError) + end + it "raises and ArgumentError when referencing an override that has not been set" do expect do Configuration.default From 44e5857ae7099d87d38d7a927b418cf4c97ae8be Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Tue, 30 Jan 2018 13:33:53 -0700 Subject: [PATCH 474/636] Add test for AlreadyConfiguredError --- spec/lib/secure_headers_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 081e8403..a60c6dfc 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -21,6 +21,13 @@ module SecureHeaders end.to raise_error(Configuration::NotYetConfiguredError) end + it "raises a AlreadyConfiguredError if trying to configure and default has already been set " do + Configuration.default + expect do + Configuration.default + end.to raise_error(Configuration::AlreadyConfiguredError) + end + it "raises and ArgumentError when referencing an override that has not been set" do expect do Configuration.default From eefa551a6a8d3233a753a95aa592c500a117de90 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Wed, 31 Jan 2018 11:53:55 -0700 Subject: [PATCH 475/636] Fix typo --- lib/secure_headers/headers/policy_management.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 204983a6..0fee2727 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -217,7 +217,7 @@ def validate_config!(config) raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override") end if !config.report_only? && config.directive_value(:report_only) - raise ContentSecurityPolicyConfigError.new("Only the csp_report_only config should sset :report_only to true") + raise ContentSecurityPolicyConfigError.new("Only the csp_report_only config should set :report_only to true") end if config.report_only? && config.directive_value(:report_only) == false From c0750dadb13005e94d9e295d98fa6e3012fca504 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Wed, 31 Jan 2018 11:54:10 -0700 Subject: [PATCH 476/636] Add changelog for 6.0.0 --- CHANGELOG.md | 4 ++++ docs/upgrading-to-6-0.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 docs/upgrading-to-6-0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a693690..460f4baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.0 + +- See the [upgrading to 5.0](docs/upgrading-to-5-0.md) guide for the breaking changes. + ## 5.0.4 - Adds support for `nonced_stylesheet_pack_tag` #373 (@paulfri) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md new file mode 100644 index 00000000..21b08e27 --- /dev/null +++ b/docs/upgrading-to-6-0.md @@ -0,0 +1,33 @@ +## Named overrides are now dynamically applied + +The original implementation of name overrides worked by making a copy of the default policy, applying the overrides, and storing the result for later use. But, this lead to unexpected results if named overrides were combined with a dynamic policy change. If a change was made to the default configuration during a request, followed by a named override, the dynamic changes would be lost. To keep things consistent named overrides have been rewritten to work the same as named appends in that they always operate on the configuration for the current request. As an example: + +```ruby +# specific opt outs +Configuration.default do |config| + config.x_frame_options = OPT_OUT +end + +# Dynamically update the default config for this request +SecureHeaders.override_x_frame_options(request, "DENY") + +SecureHeaders::Configuration.override(:dynamic_override) do |config| + config.x_content_type_options = "nosniff" +end + +SecureHeaders.use_secure_headers_override(request, :dynamic_override) +``` + +Prior to 6.0.0, the response would NOT include an `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` to `nosniff`. + +## `ContentSecurityPolicyConfig#merge` and `ContentSecurityPolicyReportOnlyConfig#merge` work more like `Hash#merge` + +These classes are typically not directly instantiated by users of SecureHeaders. But, if you access `config.csp` you end up accessing one of these objects. Prior to 6.0.0, `#merge` worked more like `#append` in that it would combine policies (i.e. if both policies contained the same key the values would be combined rather than overwritten). This was not consistent with `#merge!`, which worked more like ruby's `Hash#merge!` (overwriting duplicate keys). As of 6.0.0, `#merge` works the same as `#merge!`, but returns a new object instead of mutating `self`. + +## `Configuration#get` has been removed + +This method is not typically directly called by users of SecureHeaders. Given that named overrides are no longer statically stored, fetching them no longer makes sense. + +## Configuration headers are no longer cached + +Prior to 6.0.0 SecureHeaders prebuilt and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performmance problem and will help to elimiate a class of bugs entirely. From e84b1f4e4887689879bfd0e647b0076372c1a28f Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Wed, 31 Jan 2018 11:55:01 -0700 Subject: [PATCH 477/636] Bump version to 6.0.0 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 00f473b0..7c0d5497 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "5.0.4" + gem.version = "6.0.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From f7521804a51d439b42ea326bb10fd54082de0884 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Wed, 31 Jan 2018 12:02:30 -0700 Subject: [PATCH 478/636] Fix copy/pasta --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 460f4baa..9db517a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -## 6.0.0 +## 6.0 -- See the [upgrading to 5.0](docs/upgrading-to-5-0.md) guide for the breaking changes. +- See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes. ## 5.0.4 From 5eb12e9aed51f7802d9c4d38ce034f0a44e62222 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Wed, 31 Jan 2018 12:04:17 -0700 Subject: [PATCH 479/636] Fix a/an use --- docs/upgrading-to-6-0.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index 21b08e27..1e991488 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -18,7 +18,7 @@ end SecureHeaders.use_secure_headers_override(request, :dynamic_override) ``` -Prior to 6.0.0, the response would NOT include an `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` to `nosniff`. +Prior to 6.0.0, the response would NOT include a `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` to `nosniff`. ## `ContentSecurityPolicyConfig#merge` and `ContentSecurityPolicyReportOnlyConfig#merge` work more like `Hash#merge` @@ -30,4 +30,8 @@ This method is not typically directly called by users of SecureHeaders. Given th ## Configuration headers are no longer cached -Prior to 6.0.0 SecureHeaders prebuilt and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performmance problem and will help to elimiate a class of bugs entirely. +Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performance problem and will help to eliminate a class of bugs entirely. + +## Configuration the default configuration more than once will result in an Exception + +Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise a `AlreadyConfiguredError` if the default configuration is setup more than once. From b7bbd2b78b99dd14a0a69a37c6c24c40581d1449 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Wed, 31 Jan 2018 12:04:56 -0700 Subject: [PATCH 480/636] Minor update --- docs/upgrading-to-6-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index 1e991488..b3ce9621 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -18,7 +18,7 @@ end SecureHeaders.use_secure_headers_override(request, :dynamic_override) ``` -Prior to 6.0.0, the response would NOT include a `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` to `nosniff`. +Prior to 6.0.0, the response would NOT include a `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` set to `nosniff`. ## `ContentSecurityPolicyConfig#merge` and `ContentSecurityPolicyReportOnlyConfig#merge` work more like `Hash#merge` From eb9af0be9ec9447ddfffd89f4d2ca8ccd350bbd3 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Wed, 31 Jan 2018 12:07:30 -0700 Subject: [PATCH 481/636] More minor wording changes --- docs/upgrading-to-6-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index b3ce9621..ca642188 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -34,4 +34,4 @@ Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded ## Configuration the default configuration more than once will result in an Exception -Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise a `AlreadyConfiguredError` if the default configuration is setup more than once. +Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once. From 15581b4e5302de19e40c59af2b723b8da5c55813 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 31 Jan 2018 10:00:16 -1000 Subject: [PATCH 482/636] update code sample in upgrade doc to more closely mirror production code Also, use shorter mix-in syntax --- docs/upgrading-to-6-0.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index ca642188..17ba4e3d 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -3,19 +3,26 @@ The original implementation of name overrides worked by making a copy of the default policy, applying the overrides, and storing the result for later use. But, this lead to unexpected results if named overrides were combined with a dynamic policy change. If a change was made to the default configuration during a request, followed by a named override, the dynamic changes would be lost. To keep things consistent named overrides have been rewritten to work the same as named appends in that they always operate on the configuration for the current request. As an example: ```ruby -# specific opt outs -Configuration.default do |config| - config.x_frame_options = OPT_OUT +class ApplicationController < ActionController::Base + Configuration.default do |config| + config.x_frame_options = OPT_OUT + end + + SecureHeaders::Configuration.override(:dynamic_override) do |config| + config.x_content_type_options = "nosniff" + end end -# Dynamically update the default config for this request -SecureHeaders.override_x_frame_options(request, "DENY") +class FooController < ApplicationController + def bar + # Dynamically update the default config for this request + override_x_frame_options("DENY") + append_content_security_policy_directives(frame_src: "3rdpartyprovider.com") -SecureHeaders::Configuration.override(:dynamic_override) do |config| - config.x_content_type_options = "nosniff" + # Override everything, discard modifications above + use_secure_headers_override(:dynamic_override) + end end - -SecureHeaders.use_secure_headers_override(request, :dynamic_override) ``` Prior to 6.0.0, the response would NOT include a `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` set to `nosniff`. From b82b56d6a6d4a78857be7217d06c0aa191f6df07 Mon Sep 17 00:00:00 2001 From: Patrick Toomey Date: Fri, 2 Feb 2018 14:56:10 -0700 Subject: [PATCH 483/636] Add a class level dup method --- lib/secure_headers/configuration.rb | 4 ++++ spec/lib/secure_headers/configuration_spec.rb | 14 ++++++++++++-- .../headers/policy_management_spec.rb | 12 ++++++------ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 7982406d..bcdf3268 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -62,6 +62,10 @@ def named_append(name, &block) @appends[name] = block end + def dup + default_config.dup + end + private # Public: perform a basic deep dup. The shallow copy provided by dup/clone diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index ebe0ac1c..05fd253b 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -16,6 +16,16 @@ module SecureHeaders expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil end + it "dup results in a copy of the default config" do + Configuration.default + original_configuration = Configuration.send(:default_config) + configuration = Configuration.dup + expect(original_configuration).not_to be(configuration) + Configuration::CONFIG_ATTRIBUTES.each do |attr| + expect(original_configuration.send(attr)).to eq(configuration.send(attr)) + end + end + it "stores an override" do Configuration.override(:test_override) do |config| config.x_frame_options = "DENY" @@ -41,7 +51,7 @@ module SecureHeaders config.cookies = OPT_OUT end - config = Configuration.send(:default_config) + config = Configuration.dup expect(config.cookies).to eq(OPT_OUT) end @@ -50,7 +60,7 @@ module SecureHeaders config.cookies = {httponly: true, secure: true, samesite: {lax: false}} end - config = Configuration.send(:default_config) + config = Configuration.dup expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}}) end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 4e237164..8c10b074 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -172,7 +172,7 @@ module SecureHeaders script_src: %w('self'), } end - default_policy = Configuration.send(:default_config) + default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, style_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) @@ -188,7 +188,7 @@ module SecureHeaders }.freeze end report_uri = "https://report-uri.io/asdf" - default_policy = Configuration.send(:default_config) + default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri]) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.value).to include("report-uri #{report_uri}") @@ -205,7 +205,7 @@ module SecureHeaders non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end - default_policy = Configuration.send(:default_config) + default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, non_default_source_additions) ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive| @@ -221,7 +221,7 @@ module SecureHeaders report_only: false } end - default_policy = Configuration.send(:default_config) + default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) @@ -235,7 +235,7 @@ module SecureHeaders block_all_mixed_content: false } end - default_policy = Configuration.send(:default_config) + default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'") @@ -245,7 +245,7 @@ module SecureHeaders Configuration.default do |config| config.csp = OPT_OUT end - default_policy = Configuration.send(:default_config) + default_policy = Configuration.dup expect do ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, script_src: %w(anothercdn.com)) end.to raise_error(ContentSecurityPolicyConfigError) From 78b8bc6811a618be095c21c90c46255d2c1a7589 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 9 Feb 2018 09:27:57 -1000 Subject: [PATCH 484/636] Deprecate Configuration#get --- lib/secure_headers.rb | 4 +-- lib/secure_headers/configuration.rb | 8 ++++-- spec/lib/secure_headers/configuration_spec.rb | 26 +++++++++---------- .../headers/policy_management_spec.rb | 12 ++++----- spec/lib/secure_headers/middleware_spec.rb | 2 +- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index a0b7a39a..83179fdb 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -196,7 +196,7 @@ def header_hash_for(request) # # name - the name of the previously configured override. def use_secure_headers_override(request, name) - if config = Configuration.get(name) + if config = Configuration.get(name, internal: true) override_secure_headers_request_config(request, config) else raise ArgumentError.new("no override by the name of #{name} has been configured") @@ -228,7 +228,7 @@ def content_security_policy_style_nonce(request) # Falls back to the global config def config_for(request, prevent_dup = false) config = request.env[SECURE_HEADERS_CONFIG] || - Configuration.get(Configuration::DEFAULT_CONFIG) + Configuration.get(Configuration::DEFAULT_CONFIG, internal: true) # Global configs are frozen, per-request configs are not. When we're not diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 0998377e..f9e7a39c 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -28,7 +28,7 @@ def default(&block) # # Returns: the newly created config def override(name, base = DEFAULT_CONFIG, &block) - unless get(base) + unless get(base, internal: true) raise NotYetConfiguredError, "#{base} policy not yet supplied" end override = @configurations[base].dup @@ -40,7 +40,11 @@ def override(name, base = DEFAULT_CONFIG, &block) # # Returns the configuration with a given name or raises a # NotYetConfiguredError if `default` has not been called. - def get(name = DEFAULT_CONFIG) + def get(name = DEFAULT_CONFIG, internal: false) + unless internal + Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#get` is deprecated. It will be removed in the next major release. Use SecureHeaders::Configuration.dup to retrieve the default config." + end + if @configurations.nil? raise NotYetConfiguredError, "Default policy not yet supplied" end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index ae8c80ac..0172c883 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -9,15 +9,15 @@ module SecureHeaders end it "has a default config" do - expect(Configuration.get(Configuration::DEFAULT_CONFIG)).to_not be_nil + expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)).to_not be_nil end it "has an 'noop' config" do - expect(Configuration.get(Configuration::NOOP_CONFIGURATION)).to_not be_nil + expect(Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)).to_not be_nil end it "precomputes headers upon creation" do - default_config = Configuration.get(Configuration::DEFAULT_CONFIG) + default_config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true) header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash| header_name, header_value = if key == :csp value["Chrome"] @@ -35,8 +35,8 @@ module SecureHeaders # do nothing, just copy it end - config = Configuration.get(:test_override) - noop = Configuration.get(Configuration::NOOP_CONFIGURATION) + config = Configuration.get(:test_override, internal: true) + noop = Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true) [:csp, :csp_report_only, :cookies].each do |key| expect(config.send(key)).to eq(noop.send(key)) end @@ -47,7 +47,7 @@ module SecureHeaders config.x_content_type_options = OPT_OUT end - expect(Configuration.get.cached_headers).to_not eq(Configuration.get(:test_override).cached_headers) + expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).cached_headers).to_not eq(Configuration.get(:test_override, internal: true).cached_headers) end it "stores an override of the global config" do @@ -55,7 +55,7 @@ module SecureHeaders config.x_frame_options = "DENY" end - expect(Configuration.get(:test_override)).to_not be_nil + expect(Configuration.get(:test_override, internal: true)).to_not be_nil end it "deep dup's config values when overriding so the original cannot be modified" do @@ -63,8 +63,8 @@ module SecureHeaders config.csp[:default_src] << "'self'" end - default = Configuration.get - override = Configuration.get(:override) + default = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true) + override = Configuration.get(:override, internal: true) expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src)) end @@ -78,9 +78,9 @@ module SecureHeaders config.csp = config.csp.merge(script_src: %w(example.org)) end - original_override = Configuration.get(:override) + original_override = Configuration.get(:override, internal: true) expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self')) - override_config = Configuration.get(:second_override) + override_config = Configuration.get(:second_override, internal: true) expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org)) end @@ -101,7 +101,7 @@ module SecureHeaders config.cookies = OPT_OUT end - config = Configuration.get + config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true) expect(config.cookies).to eq(OPT_OUT) end @@ -110,7 +110,7 @@ module SecureHeaders config.cookies = {httponly: true, secure: true, samesite: {lax: false}} end - config = Configuration.get + config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true) expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}}) end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 24d35cbc..472f30b0 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -152,7 +152,7 @@ module SecureHeaders script_src: %w('self'), } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, style_src: %w(anothercdn.com)) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, style_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com") @@ -167,7 +167,7 @@ module SecureHeaders }.freeze end report_uri = "https://report-uri.io/asdf" - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri]) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, report_uri: [report_uri]) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.value).to include("report-uri #{report_uri}") end @@ -183,7 +183,7 @@ module SecureHeaders non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, non_default_source_additions) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, non_default_source_additions) ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) @@ -198,7 +198,7 @@ module SecureHeaders report_only: false } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_only: true) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, report_only: true) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) end @@ -211,7 +211,7 @@ module SecureHeaders block_all_mixed_content: false } end - combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true) + combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'") end @@ -221,7 +221,7 @@ module SecureHeaders config.csp = OPT_OUT end expect do - ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) + ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, script_src: %w(anothercdn.com)) end.to raise_error(ContentSecurityPolicyConfigError) end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index cc49cb5a..32a8320f 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -50,7 +50,7 @@ module SecureHeaders end request = Rack::Request.new({}) SecureHeaders.use_secure_headers_override(request, "my_custom_config") - expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config")) + expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config", internal: true)) _, env = middleware.call request.env expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org") end From 98ac406b0b33cb7d0a9ab312e6f598af64ec5460 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 9 Feb 2018 10:42:47 -1000 Subject: [PATCH 485/636] rubocop --- lib/secure_headers/headers/clear_site_data.rb | 2 +- spec/lib/secure_headers/middleware_spec.rb | 2 +- spec/lib/secure_headers/view_helpers_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index 34f09d0d..e5e4ebf8 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -48,7 +48,7 @@ def validate_config!(config) # # Returns a String of quoted values that are comma separated. def make_header_value(types) - types.map { |t| "\"#{t}\""}.join(", ") + types.map { |t| %("#{t}") }.join(", ") end end end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 32a8320f..ec76f7f3 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -66,7 +66,7 @@ module SecureHeaders end it "allows opting out of cookie protection with OPT_OUT alone" do - Configuration.default { |config| config.cookies = OPT_OUT} + Configuration.default { |config| config.cookies = OPT_OUT } # do NOT make this request https. non-https requests modify a config, # causing an exception when operating on OPT_OUT. This ensures we don't diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 54387dbf..cf5c67f5 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -67,7 +67,7 @@ def content_tag(type, content = nil, options = nil, &block) end if options.is_a?(Hash) - options = options.map {|k, v| " #{k}=#{v}"} + options = options.map { |k, v| " #{k}=#{v}" } end "<#{type}#{options}>#{content}" end From 60fd2821be640db6b85aefbbfbf515f6d309e08d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 12 Feb 2018 10:13:44 -1000 Subject: [PATCH 486/636] add test verifying deprecation notice is posted --- Gemfile | 1 + spec/lib/secure_headers/configuration_spec.rb | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Gemfile b/Gemfile index 0a64a943..7fcc80b2 100644 --- a/Gemfile +++ b/Gemfile @@ -19,4 +19,5 @@ group :guard do gem "growl" gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24] gem "rb-fsevent" + gem 'terminal-notifier-guard' end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 0172c883..87f1c32e 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -12,6 +12,11 @@ module SecureHeaders expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)).to_not be_nil end + it "warns when using deprecated internal-ish #get API" do + expect(Kernel).to receive(:warn).once.with(/`#get` is deprecated/) + Configuration.get(Configuration::DEFAULT_CONFIG) + end + it "has an 'noop' config" do expect(Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)).to_not be_nil end From 0b9b4431a599779c380f0b2aafd188503dee9ce0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 12 Feb 2018 10:30:35 -1000 Subject: [PATCH 487/636] lint --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 7fcc80b2..39d06141 100644 --- a/Gemfile +++ b/Gemfile @@ -19,5 +19,5 @@ group :guard do gem "growl" gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24] gem "rb-fsevent" - gem 'terminal-notifier-guard' + gem "terminal-notifier-guard" end From cdfbe4dbf94ccfd14ce673fb15374edfc5c8cb3e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 12 Feb 2018 12:42:54 -1000 Subject: [PATCH 488/636] bump to 5.0.5 --- CHANGELOG.md | 4 ++++ secure_headers.gemspec | 11 +---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a693690..fe47935d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.5 + +A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x + ## 5.0.4 - Adds support for `nonced_stylesheet_pack_tag` #373 (@paulfri) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 00f473b0..a3e32021 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "5.0.4" + gem.version = "5.0.5" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." @@ -17,13 +17,4 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.add_development_dependency "rake" gem.add_dependency "useragent", ">= 0.15.0" - - # TODO: delete this after 4.1 is cut or a number of 4.0.x releases have occurred - gem.post_install_message = <<-POST_INSTALL - -********** -:wave: secure_headers 5.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-5-0.md -********** - - POST_INSTALL end From 41d0e688d60debee1574d85aded536281f06b04d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 13 Feb 2018 11:12:48 -1000 Subject: [PATCH 489/636] lint --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 07687bbc..3f382a2b 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -213,7 +213,7 @@ def populate_nonces(directive, source_list) # unsafe-inline, this is more concise. def append_nonce(source_list, nonce) if nonce - source_list.push(*["'nonce-#{nonce}'", UNSAFE_INLINE]) + source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE) end source_list From 69568331453b5341c9663de64fe6b8c694f5c247 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 13 Feb 2018 11:16:38 -1000 Subject: [PATCH 490/636] add note about sending nonces unconditionally --- docs/upgrading-to-6-0.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index 17ba4e3d..acae3b3b 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -42,3 +42,9 @@ Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded ## Configuration the default configuration more than once will result in an Exception Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once. + +## Nonce behavior and console warnings + +Since the first commit, reducing browser console messages was a goal. It led to overly complicated and error-prone UA sniffing. Nowadays, consoles warn on completely legitimate use of features meant to be backwards compatible. So the goal is impossible and the impact is negative, so eliminating code using sniffing is a goal. + +The first example: we will now send `'unsafe-inline'` along with nonce source expressions. This will generate warnings in some consoles but is 100% valid use and was a design goal of CSP in the early days. The concept of versioning CSP lost out and so we're left with backward compatibility as our only option. From 098f26acb30e643a755d6ae21d068c8b81c61639 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 13 Feb 2018 11:20:44 -1000 Subject: [PATCH 491/636] add ruby 2.5 --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 30723457..8fbe5872 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,10 @@ language: ruby rvm: - ruby-head - - 2.4.2 - - 2.3.5 - - 2.2.8 + - 2.5.0 + - 2.4.3 + - 2.3.6 + - 2.2.9 - jruby-head env: From 2a1d16acc46572490e01f6400a553f72dbaccf51 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 13 Feb 2018 11:24:34 -1000 Subject: [PATCH 492/636] Update rubygems --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8fbe5872..4f3b578c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,10 @@ matrix: - rvm: jruby-head - rvm: ruby-head -before_install: gem update bundler +before_install: + - gem update --system + - gem --version + - gem update bundler bundler_args: --without guard -j 3 sudo: false From 23b37d00929e5bbd4630565798508e5b5b071c19 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 21 Feb 2018 11:36:26 -1000 Subject: [PATCH 493/636] Update docs to encourage using `default-src 'none'` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d79d386d..59ff75d9 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ SecureHeaders::Configuration.default do |config| preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives - default_src: %w(https: 'self'), + default_src: %w('none'), base_uri: %w('self'), block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. From 5c47914f9c481d8c69fb7af141ed5a79b213bfa1 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 21 Feb 2018 11:40:07 -1000 Subject: [PATCH 494/636] Update note about current version status. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59ff75d9..1d4088b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) -**master represents 5.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md) and [upgrading to 5.x doc](docs/upgrading-to-5-0.md) for instructions on how to upgrade. Bug fixes should go in the 3.x branch for now. +**master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. **The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](docs/upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. From d1f0162eb678554f293b99e838a996cdccea579d Mon Sep 17 00:00:00 2001 From: Eugene Kenny Date: Mon, 26 Mar 2018 00:46:33 +0100 Subject: [PATCH 495/636] Avoid calling content_security_policy_nonce internally Rails 5.2 adds support for configuring a Content-Security-Policy header, including adding nonces to tags produced by the `javascript_tag` helper. Unfortunately, Rails and this gem now both define a helper named `content_security_policy_nonce`: https://github.com/rails/rails/blob/v5.2.0.rc2/actionpack/lib/action_controller/metal/content_security_policy.rb#L44 https://github.com/twitter/secureheaders/blob/v5.0.5/lib/secure_headers/view_helper.rb#L69 The Rails helper wins over the Secure Headers one, and helpers like `nonced_javascript_tag` currently raise this error on Rails 5.2: ArgumentError: wrong number of arguments (given 1, expected 0) By using a method with a different name internally, we avoid clashing with the Rails implementation, and `nonced_javascript_tag` works again. --- lib/secure_headers/view_helper.rb | 17 ++++++------ spec/lib/secure_headers/view_helpers_spec.rb | 28 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 0299fec0..89344231 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -19,7 +19,7 @@ def nonced_style_tag(content_or_options = {}, &block) # # Returns an html-safe link tag with the nonce attribute. def nonced_stylesheet_link_tag(*args, &block) - opts = extract_options(args).merge(nonce: content_security_policy_nonce(:style)) + opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style)) stylesheet_link_tag(*args, opts, &block) end @@ -37,7 +37,7 @@ def nonced_javascript_tag(content_or_options = {}, &block) # # Returns an html-safe script tag with the nonce attribute. def nonced_javascript_include_tag(*args, &block) - opts = extract_options(args).merge(nonce: content_security_policy_nonce(:script)) + opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script)) javascript_include_tag(*args, opts, &block) end @@ -47,7 +47,7 @@ def nonced_javascript_include_tag(*args, &block) # # Returns an html-safe script tag with the nonce attribute. def nonced_javascript_pack_tag(*args, &block) - opts = extract_options(args).merge(nonce: content_security_policy_nonce(:script)) + opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script)) javascript_pack_tag(*args, opts, &block) end @@ -57,7 +57,7 @@ def nonced_javascript_pack_tag(*args, &block) # # Returns an html-safe link tag with the nonce attribute. def nonced_stylesheet_pack_tag(*args, &block) - opts = extract_options(args).merge(nonce: content_security_policy_nonce(:style)) + opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style)) stylesheet_pack_tag(*args, opts, &block) end @@ -66,7 +66,7 @@ def nonced_stylesheet_pack_tag(*args, &block) # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns a non-html-safe nonce value. - def content_security_policy_nonce(type) + def _content_security_policy_nonce(type) case type when :script SecureHeaders.content_security_policy_script_nonce(@_request) @@ -74,13 +74,14 @@ def content_security_policy_nonce(type) SecureHeaders.content_security_policy_style_nonce(@_request) end end + alias_method :content_security_policy_nonce, :_content_security_policy_nonce def content_security_policy_script_nonce - content_security_policy_nonce(:script) + _content_security_policy_nonce(:script) end def content_security_policy_style_nonce - content_security_policy_nonce(:style) + _content_security_policy_nonce(:style) end ## @@ -152,7 +153,7 @@ def nonced_tag(type, content_or_options, block) else content_or_options.html_safe # :'( end - content_tag type, content, options.merge(nonce: content_security_policy_nonce(type)) + content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type)) end def extract_options(args) diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index 67cc0d22..c71534a1 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -97,6 +97,12 @@ def request end end +class MessageWithConflictingMethod < Message + def content_security_policy_nonce + "rails-nonce" + end +end + module SecureHeaders describe ViewHelpers do let(:app) { lambda { |env| [200, env, "app"] } } @@ -159,5 +165,27 @@ module SecureHeaders expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) end end + + it "avoids calling content_security_policy_nonce internally" do + begin + allow(SecureRandom).to receive(:base64).and_return("abc123") + + expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8=" + Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"]) + expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc=" + Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"]) + + # render erb that calls out to helpers. + MessageWithConflictingMethod.new(request).result + _, env = middleware.call request.env + + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/) + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/) + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/) + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) + + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).not_to match(/rails-nonce/) + end + end end end From fd363b7ea6de73df2f877682570944785c606931 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 26 Mar 2018 13:03:53 -1000 Subject: [PATCH 496/636] Add note about `content_security_policy_nonce` name change --- docs/upgrading-to-6-0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index acae3b3b..2c9e4daf 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -35,6 +35,10 @@ These classes are typically not directly instantiated by users of SecureHeaders. This method is not typically directly called by users of SecureHeaders. Given that named overrides are no longer statically stored, fetching them no longer makes sense. +## `content_security_policy_nonce` has been renamed + +While tag helpers such as `nonced_javascript_tag` and `nonced_style_tag`, the value for the nonce was available via `content_security_policy_nonce`. Rails 5.2 has implemented a method with the same name but clashes with the number of arguments. It has been renamed to`_content_security_policy_nonce` and will likely be removed in future versions. + ## Configuration headers are no longer cached Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performance problem and will help to eliminate a class of bugs entirely. From 1006cbab200b6160799e2f5fd8675754f11afdf1 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 26 Mar 2018 13:04:51 -1000 Subject: [PATCH 497/636] bumpt to 6.0 alpha 2 --- CHANGELOG.md | 4 ++-- secure_headers.gemspec | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 601fccca..95dc7dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## 6.0 - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes. -======= + ## 5.0.5 -A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x +- A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x ## 5.0.4 diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 46af358b..b0d8f7dc 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "6.0.0.alpha01" + gem.version = "6.0.0.alpha02" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 8378f13490e5e287e4092091faad579a5337a1ba Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 27 Mar 2018 08:20:33 -1000 Subject: [PATCH 498/636] Whoops, I missed the alias_method --- docs/upgrading-to-6-0.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index 2c9e4daf..acae3b3b 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -35,10 +35,6 @@ These classes are typically not directly instantiated by users of SecureHeaders. This method is not typically directly called by users of SecureHeaders. Given that named overrides are no longer statically stored, fetching them no longer makes sense. -## `content_security_policy_nonce` has been renamed - -While tag helpers such as `nonced_javascript_tag` and `nonced_style_tag`, the value for the nonce was available via `content_security_policy_nonce`. Rails 5.2 has implemented a method with the same name but clashes with the number of arguments. It has been renamed to`_content_security_policy_nonce` and will likely be removed in future versions. - ## Configuration headers are no longer cached Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performance problem and will help to eliminate a class of bugs entirely. From 64c0acb4e778e9faa35631ef5fc3b94abea8350d Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 10 Apr 2018 15:29:27 -1000 Subject: [PATCH 499/636] Remove all useragent sniffing --- lib/secure_headers.rb | 4 +- lib/secure_headers/configuration.rb | 4 +- .../headers/content_security_policy.rb | 63 ++---------------- .../headers/policy_management.rb | 50 +------------- secure_headers.gemspec | 1 - .../headers/content_security_policy_spec.rb | 65 +------------------ .../headers/policy_management_spec.rb | 4 +- spec/lib/secure_headers_spec.rb | 37 ----------- 8 files changed, 13 insertions(+), 215 deletions(-) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index f427e988..9926fb2b 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -15,7 +15,6 @@ require "secure_headers/middleware" require "secure_headers/railtie" require "secure_headers/view_helper" -require "useragent" require "singleton" require "secure_headers/configuration" @@ -149,8 +148,7 @@ def header_hash_for(request) prevent_dup = true config = config_for(request, prevent_dup) config.validate_config! - user_agent = UserAgent.parse(request.user_agent) - headers = config.generate_headers(user_agent) + headers = config.generate_headers if request.scheme != HTTPS HTTPS_HEADER_CLASSES.each do |klass| diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index bcdf3268..159e8762 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -194,11 +194,11 @@ def override(name = nil, &block) self end - def generate_headers(user_agent) + def generate_headers headers = {} HEADERABLE_ATTRIBUTES.each do |attr| klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr] - header_name, value = klass.make_header(instance_variable_get("@#{attr}"), user_agent) + header_name, value = klass.make_header(instance_variable_get("@#{attr}")) if header_name && value headers[header_name] = value end diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 3f382a2b..f6af2f79 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -1,19 +1,12 @@ # frozen_string_literal: true require_relative "policy_management" require_relative "content_security_policy_config" -require "useragent" module SecureHeaders class ContentSecurityPolicy include PolicyManagement - # constants to be used for version-specific UA sniffing - VERSION_46 = ::UserAgent::Version.new("46") - VERSION_10 = ::UserAgent::Version.new("10") - FALLBACK_VERSION = ::UserAgent::Version.new("0") - - def initialize(config = nil, user_agent = OTHER) - user_agent ||= OTHER + def initialize(config = nil) @config = if config.is_a?(Hash) if config[:report_only] ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG) @@ -26,12 +19,6 @@ def initialize(config = nil, user_agent = OTHER) config end - @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base) - user_agent - else - UserAgent.parse(user_agent) - end - @frame_src = normalize_child_frame_src @preserve_schemes = @config.preserve_schemes @script_nonce = @config.script_nonce @style_nonce = @config.style_nonce @@ -56,20 +43,10 @@ def value private - def normalize_child_frame_src - if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src - raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers." - end - - @config.frame_src || @config.child_src - end - # Private: converts the config object into a string representing a policy. # Places default-src at the first directive and report-uri as the last. All # others are presented in alphabetical order. # - # Unsupported directives are filtered based on the user agent. - # # Returns a content security policy header value. def build_value directives.map do |directive_name| @@ -125,18 +102,7 @@ def build_media_type_list_directive(directive) # # Returns a string representing a directive. def build_source_list_directive(directive) - source_list = case directive - when :child_src - if supported_directives.include?(:child_src) - @frame_src - end - when :frame_src - unless supported_directives.include?(:child_src) - @frame_src - end - else - @config.directive_value(directive) - end + source_list = @config.directive_value(directive) if source_list != OPT_OUT && source_list && source_list.any? normalized_source_list = minify_source_list(directive, source_list) @@ -219,13 +185,13 @@ def append_nonce(source_list, nonce) source_list end - # Private: return the list of directives that are supported by the user agent, + # Private: return the list of directives, # starting with default-src and ending with report-uri. def directives [ DEFAULT_SRC, - BODY_DIRECTIVES.select { |key| supported_directives.include?(key) }, - REPORT_URI + BODY_DIRECTIVES, + REPORT_URI, ].flatten end @@ -234,25 +200,6 @@ def strip_source_schemes(source_list) source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } end - # Private: determine which directives are supported for the given user agent. - # - # Add UA-sniffing special casing here. - # - # Returns an array of symbols representing the directives. - def supported_directives - @supported_directives ||= if VARIATIONS[@parsed_ua.browser] - if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46) - VARIATIONS["FirefoxTransitional"] - elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10) - VARIATIONS["SafariTransitional"] - else - VARIATIONS[@parsed_ua.browser] - end - else - VARIATIONS[OTHER] - end - end - def symbol_to_hyphen_case(sym) sym.to_s.tr("_", "-") end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 00b9f052..c204f073 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -81,58 +81,12 @@ def self.included(base) UPGRADE_INSECURE_REQUESTS ].flatten.freeze - EDGE_DIRECTIVES = DIRECTIVES_1_0 - SAFARI_DIRECTIVES = DIRECTIVES_1_0 - SAFARI_10_DIRECTIVES = DIRECTIVES_2_0 - - FIREFOX_UNSUPPORTED_DIRECTIVES = [ - BLOCK_ALL_MIXED_CONTENT, - CHILD_SRC, - WORKER_SRC, - PLUGIN_TYPES - ].freeze - - FIREFOX_46_DEPRECATED_DIRECTIVES = [ - FRAME_SRC - ].freeze - - FIREFOX_46_UNSUPPORTED_DIRECTIVES = [ - BLOCK_ALL_MIXED_CONTENT, - WORKER_SRC, - PLUGIN_TYPES - ].freeze - - FIREFOX_DIRECTIVES = ( - DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES - ).freeze - - FIREFOX_46_DIRECTIVES = ( - DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES - ).freeze - - CHROME_DIRECTIVES = ( - DIRECTIVES_3_0 - ).freeze - ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort # Think of default-src and report-uri as the beginning and end respectively, # everything else is in between. BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] - VARIATIONS = { - "Chrome" => CHROME_DIRECTIVES, - "Opera" => CHROME_DIRECTIVES, - "Firefox" => FIREFOX_DIRECTIVES, - "FirefoxTransitional" => FIREFOX_46_DIRECTIVES, - "Safari" => SAFARI_DIRECTIVES, - "SafariTransitional" => SAFARI_10_DIRECTIVES, - "Edge" => EDGE_DIRECTIVES, - "Other" => CHROME_DIRECTIVES - }.freeze - - OTHER = "Other".freeze - DIRECTIVE_VALUE_TYPES = { BASE_URI => :source_list, BLOCK_ALL_MIXED_CONTENT => :boolean, @@ -199,9 +153,9 @@ module ClassMethods # # Returns a default policy if no configuration is provided, or a # header name and value based on the config. - def make_header(config, user_agent = nil) + def make_header(config) return if config.nil? || config == OPT_OUT - header = new(config, user_agent) + header = new(config) [header.name, header.value] end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index b0d8f7dc..cbf0a3eb 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -16,5 +16,4 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_development_dependency "rake" - gem.add_dependency "useragent", ">= 0.15.0" end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index f2462711..04b98926 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -116,73 +116,10 @@ module SecureHeaders ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value end - it "raises an error when child-src and frame-src are supplied but are not equal" do - expect { - ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value - }.to raise_error(ArgumentError) - end - it "supports strict-dynamic" do - csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome]) + csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'") end - - context "browser sniffing" do - let (:complex_opts) do - (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash| - hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"] - end.merge({ - block_all_mixed_content: true, - upgrade_insecure_requests: true, - script_src: %w(script-src.com), - script_nonce: 123456, - sandbox: %w(allow-forms), - plugin_types: %w(application/pdf) - }) - end - - it "does not filter any directives for Chrome" do - policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com") - end - - it "does not filter any directives for Opera" do - policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com") - end - - it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do - policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") - end - - it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do - policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") - end - - it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for Edge" do - policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge]) - expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") - end - - it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for safari" do - policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) - expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") - end - - it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, and hash sources for safari 10 and higher" do - policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10]) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") - end - - it "falls back to standard Firefox defaults when the useragent version is not present" do - ua = USER_AGENTS[:firefox].dup - allow(ua).to receive(:version).and_return(nil) - policy = ContentSecurityPolicy.new(complex_opts, ua) - expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") - end - end end end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 8c10b074..d0cb0cea 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -190,7 +190,7 @@ module SecureHeaders report_uri = "https://report-uri.io/asdf" default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri]) - csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) + csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to include("report-uri #{report_uri}") end @@ -223,7 +223,7 @@ module SecureHeaders end default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true) - csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) + csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 0fc87481..6ef9d05b 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -127,27 +127,6 @@ module SecureHeaders expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN) end - it "produces a UA-specific CSP when overriding (and busting the cache)" do - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - script_src: %w('self'), - child_src: %w('self') - } - end - firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox])) - - # append an unsupported directive - SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(application/pdf)}) - # append a supported directive - SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')}) - - hash = SecureHeaders.header_hash_for(firefox_request) - - # child-src is translated to frame-src - expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'") - end - it "produces a hash of headers with default config" do Configuration.default hash = SecureHeaders.header_hash_for(request) @@ -197,22 +176,6 @@ module SecureHeaders expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end - it "child-src and frame-src must match" do - Configuration.default do |config| - config.csp = { - default_src: %w('self'), - frame_src: %w(frame_src.com), - script_src: %w('self') - } - end - - SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com)) - - expect { - SecureHeaders.header_hash_for(chrome_request) - }.to raise_error(ArgumentError) - end - it "supports named appends" do Configuration.default do |config| config.csp = { From 1ac621f8b55829444b888602f6c9897931c84855 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 11 Apr 2018 08:28:21 -1000 Subject: [PATCH 500/636] remove another case of frame/child src mucking --- lib/secure_headers/headers/policy_management.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index c204f073..acfa6793 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -257,17 +257,11 @@ def populate_fetch_source_with_default!(original, additions) # Don't set a default if directive has an existing value next if original[directive] if FETCH_SOURCES.include?(directive) - original[directive] = default_for(directive, original) + original[directive] = original[DEFAULT_SRC] end end end - def default_for(directive, original) - return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC] - return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC] - original[DEFAULT_SRC] - end - def source_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :source_list end From fbf0ec1afea04769be78d00c55433e410868c1b2 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 11 Apr 2018 08:28:48 -1000 Subject: [PATCH 501/636] add note to upgrade doc about frame/child src mess --- docs/upgrading-to-6-0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index acae3b3b..98726eb4 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -48,3 +48,7 @@ Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` Since the first commit, reducing browser console messages was a goal. It led to overly complicated and error-prone UA sniffing. Nowadays, consoles warn on completely legitimate use of features meant to be backwards compatible. So the goal is impossible and the impact is negative, so eliminating code using sniffing is a goal. The first example: we will now send `'unsafe-inline'` along with nonce source expressions. This will generate warnings in some consoles but is 100% valid use and was a design goal of CSP in the early days. The concept of versioning CSP lost out and so we're left with backward compatibility as our only option. + +## No more frame-src/child-src magic + +First there was frame-src. Then there was child-src which deprecated frame-src. Then child-src became deprecated in favor of frame-src and worker-src. In the meantime, every browser did something different. For a while, it was recommended to set child-src and frame-src but the values had to be identical. secure_headers would sniff the UA to determine whether to use frame or child src. Now that the dust has settled, I think we can stop sniffing UAs and just go with a straightforward application. From ecc8bb0b3661b79767ea21020e420ab62ff5b47c Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 11 Apr 2018 08:30:34 -1000 Subject: [PATCH 502/636] a little more clarification around the child/frame-src problem --- docs/upgrading-to-6-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index 98726eb4..3ae0becc 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -51,4 +51,4 @@ The first example: we will now send `'unsafe-inline'` along with nonce source ex ## No more frame-src/child-src magic -First there was frame-src. Then there was child-src which deprecated frame-src. Then child-src became deprecated in favor of frame-src and worker-src. In the meantime, every browser did something different. For a while, it was recommended to set child-src and frame-src but the values had to be identical. secure_headers would sniff the UA to determine whether to use frame or child src. Now that the dust has settled, I think we can stop sniffing UAs and just go with a straightforward application. +First there was frame-src. Then there was child-src which deprecated frame-src. Then child-src became deprecated in favor of frame-src and worker-src. In the meantime, every browser did something different. For a while, it was recommended to set child-src and frame-src but the values had to be identical. secure_headers would sniff the UA to determine whether to use frame or child src. That can lead to confusing things like setting frame-src but seeing child-src. If the child-src and frame-src did not match up, an error is raised. This can be very confusing when using dynamic overrides ("Do we use child-src or frame-src?" => :boom:). Now that the dust has settled, I think we can stop sniffing UAs and just go with a straightforward application. From 8453f46089a0aeee5204ee56cf1a69dcc39d889b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 1 May 2018 12:52:41 -1000 Subject: [PATCH 503/636] bump to 6 alpha 3 --- docs/upgrading-to-6-0.md | 10 +++------- secure_headers.gemspec | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index 3ae0becc..cb92effd 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -43,12 +43,8 @@ Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once. -## Nonce behavior and console warnings +## All user agent sniffing has been removed -Since the first commit, reducing browser console messages was a goal. It led to overly complicated and error-prone UA sniffing. Nowadays, consoles warn on completely legitimate use of features meant to be backwards compatible. So the goal is impossible and the impact is negative, so eliminating code using sniffing is a goal. +The policy configured is the policy that is delivered in terms of which directives are sent. We still dedup, strip schemes, and look for other optimizations but we will not e.g. conditionally send `frame-src` / `child-src` or apply `nonce`s / `unsafe-inline`. -The first example: we will now send `'unsafe-inline'` along with nonce source expressions. This will generate warnings in some consoles but is 100% valid use and was a design goal of CSP in the early days. The concept of versioning CSP lost out and so we're left with backward compatibility as our only option. - -## No more frame-src/child-src magic - -First there was frame-src. Then there was child-src which deprecated frame-src. Then child-src became deprecated in favor of frame-src and worker-src. In the meantime, every browser did something different. For a while, it was recommended to set child-src and frame-src but the values had to be identical. secure_headers would sniff the UA to determine whether to use frame or child src. That can lead to confusing things like setting frame-src but seeing child-src. If the child-src and frame-src did not match up, an error is raised. This can be very confusing when using dynamic overrides ("Do we use child-src or frame-src?" => :boom:). Now that the dust has settled, I think we can stop sniffing UAs and just go with a straightforward application. +The primary reason for these per-browser customization was to reduce console warnings. This has lead to many bugs and results inc confusing behavior. Also, console logs are incredibly noisy today and increasingly warn you about perfectly valid things (like sending `X-Frame-Options` and `frame-ancestors` together). diff --git a/secure_headers.gemspec b/secure_headers.gemspec index cbf0a3eb..91f74ea4 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "6.0.0.alpha02" + gem.version = "6.0.0.alpha03" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 9ccefc1d9794468cd2fac03a94e22b48ccb1521e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 8 May 2018 09:36:00 -1000 Subject: [PATCH 504/636] bump to 6.0 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 91f74ea4..c73db792 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -2,7 +2,7 @@ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "6.0.0.alpha03" + gem.version = "6.0.0" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 14d697ac367d0645db3fcae7a45f04e244c0289e Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 20 Jul 2018 06:51:55 -1000 Subject: [PATCH 505/636] Add support for navigate-to, prefetch-src, and require-sri-for Fixes https://github.com/twitter/secure_headers/issues/387 Fixes https://github.com/twitter/secure_headers/issues/388 Fixes https://github.com/twitter/secure_headers/issues/264 --- .../headers/content_security_policy.rb | 4 +- .../headers/content_security_policy_config.rb | 3 ++ .../headers/policy_management.rb | 42 ++++++++++++++++--- .../headers/content_security_policy_spec.rb | 25 +++++++++++ .../headers/policy_management_spec.rb | 23 ++++++---- 5 files changed, 81 insertions(+), 16 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index f6af2f79..c744ebce 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -51,14 +51,14 @@ def value def build_value directives.map do |directive_name| case DIRECTIVE_VALUE_TYPES[directive_name] + when :source_list, :require_sri_for_list # require_sri is a simple set of strings that don't need to deal with symbol casing + build_source_list_directive(directive_name) when :boolean symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) when :sandbox_list build_sandbox_list_directive(directive_name) when :media_type_list build_media_type_list_directive(directive_name) - when :source_list - build_source_list_directive(directive_name) end end.compact.join("; ") end diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 9acf6212..352739d8 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -27,11 +27,14 @@ def initialize(hash) @img_src = nil @manifest_src = nil @media_src = nil + @navigate_to = nil @object_src = nil @plugin_types = nil + @prefetch_src = nil @preserve_schemes = nil @report_only = nil @report_uri = nil + @require_sri_for = nil @sandbox = nil @script_nonce = nil @script_src = nil diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index acfa6793..a6e6d4c8 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -1,4 +1,7 @@ # frozen_string_literal: true + +require "set" + module SecureHeaders module PolicyManagement def self.included(base) @@ -70,6 +73,9 @@ def self.included(base) # https://w3c.github.io/webappsec/specs/CSP2/ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content MANIFEST_SRC = :manifest_src + NAVIGATE_TO = :navigate_to + PREFETCH_SRC = :prefetch_src + REQUIRE_SRI_FOR = :require_sri_for UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests WORKER_SRC = :worker_src @@ -77,6 +83,9 @@ def self.included(base) DIRECTIVES_2_0, BLOCK_ALL_MIXED_CONTENT, MANIFEST_SRC, + NAVIGATE_TO, + PREFETCH_SRC, + REQUIRE_SRI_FOR, WORKER_SRC, UPGRADE_INSECURE_REQUESTS ].flatten.freeze @@ -100,14 +109,17 @@ def self.included(base) IMG_SRC => :source_list, MANIFEST_SRC => :source_list, MEDIA_SRC => :source_list, + NAVIGATE_TO => :source_list, OBJECT_SRC => :source_list, PLUGIN_TYPES => :media_type_list, + REQUIRE_SRI_FOR => :require_sri_for_list, REPORT_URI => :source_list, + PREFETCH_SRC => :source_list, SANDBOX => :sandbox_list, SCRIPT_SRC => :source_list, STYLE_SRC => :source_list, WORKER_SRC => :source_list, - UPGRADE_INSECURE_REQUESTS => :boolean + UPGRADE_INSECURE_REQUESTS => :boolean, }.freeze # These are directives that don't have use a source list, and hence do not @@ -122,7 +134,8 @@ def self.included(base) BASE_URI, FORM_ACTION, FRAME_ANCESTORS, - REPORT_URI + NAVIGATE_TO, + REPORT_URI, ] FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES @@ -148,6 +161,8 @@ def self.included(base) :style_nonce ].freeze + REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style)) + module ClassMethods # Public: generate a header name, value array that is user-agent-aware. # @@ -241,7 +256,8 @@ def merge_policy_additions(original, additions) def list_directive?(directive) source_list?(directive) || sandbox_list?(directive) || - media_type_list?(directive) + media_type_list?(directive) || + require_sri_for_list?(directive) end # For each directive in additions that does not exist in the original config, @@ -274,11 +290,17 @@ def media_type_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :media_type_list end + def require_sri_for_list?(directive) + DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list + end + # Private: Validates that the configuration has a valid type, or that it is a valid # source expression. def validate_directive!(directive, value) ensure_valid_directive!(directive) case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive] + when :source_list + validate_source_expression!(directive, value) when :boolean unless boolean?(value) raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value") @@ -287,8 +309,8 @@ def validate_directive!(directive, value) validate_sandbox_expression!(directive, value) when :media_type_list validate_media_type_expression!(directive, value) - when :source_list - validate_source_expression!(directive, value) + when :require_sri_for_list + validate_require_sri_source_expression!(directive, value) else raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") end @@ -323,6 +345,16 @@ def validate_media_type_expression!(directive, media_type_expression) end end + # Private: validates that a require sri for expression: + # 1. is an array of strings + # 2. is a subset of ["string", "style"] + def validate_require_sri_source_expression!(directive, require_sri_for_expression) + ensure_array_of_strings!(directive, require_sri_for_expression) + unless require_sri_for_expression.to_set.subset?(REQUIRE_SRI_FOR_VALUES) + raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_SRI_FOR_VALUES.to_a} but was #{require_sri_for_expression})) + end + end + # Private: validates that a source expression: # 1. is an array of strings # 2. does not contain any deprecated, now invalid values (inline, eval, self, none) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 04b98926..f5d38be5 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -116,6 +116,31 @@ module SecureHeaders ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value end + it "allows script as a require-sri-src" do + csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script)) + expect(csp.value).to eq("default-src 'self'; require-sri-for script") + end + + it "allows style as a require-sri-src" do + csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(style)) + expect(csp.value).to eq("default-src 'self'; require-sri-for style") + end + + it "allows script and style as a require-sri-src" do + csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script style)) + expect(csp.value).to eq("default-src 'self'; require-sri-for script style") + end + + it "includes prefetch-src" do + csp = ContentSecurityPolicy.new(default_src: %w('self'), prefetch_src: %w(foo.com)) + expect(csp.value).to eq("default-src 'self'; prefetch-src foo.com") + end + + it "includes navigate-to" do + csp = ContentSecurityPolicy.new(default_src: %w('self'), navigate_to: %w(foo.com)) + expect(csp.value).to eq("default-src 'self'; navigate-to foo.com") + end + it "supports strict-dynamic" do csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'") diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index d0cb0cea..4258da08 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -28,24 +28,29 @@ module SecureHeaders # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), - frame_src: %w('self' *.twimg.com itunes.apple.com), - child_src: %w('self' *.twimg.com itunes.apple.com), + + base_uri: %w('self'), + block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) connect_src: %w(wss:), + child_src: %w('self' *.twimg.com itunes.apple.com), font_src: %w('self' data:), + form_action: %w('self' github.com), + frame_ancestors: %w('none'), + frame_src: %w('self' *.twimg.com itunes.apple.com), img_src: %w(mycdn.com data:), manifest_src: %w(manifest.com), media_src: %w(utoob.com), + navigate_to: %w(netscape.com), object_src: %w('self'), + plugin_types: %w(application/x-shockwave-flash), + prefetch_src: %w(fetch.com), + require_sri_for: %w(script style), script_src: %w('self'), style_src: %w('unsafe-inline'), - worker_src: %w(worker.com), - base_uri: %w('self'), - form_action: %w('self' github.com), - frame_ancestors: %w('none'), - plugin_types: %w(application/x-shockwave-flash), - block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ - report_uri: %w(https://example.com/uri-directive) + worker_src: %w(worker.com), + + report_uri: %w(https://example.com/uri-directive), } ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config)) From 90597531a705ddb5fe9cd3ec222191786068dc27 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 20 Jul 2018 07:02:27 -1000 Subject: [PATCH 506/636] Update secure cookie error message when providing 'false' Fixes https://github.com/twitter/secure_headers/issues/393 --- lib/secure_headers/utils/cookies_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/utils/cookies_config.rb b/lib/secure_headers/utils/cookies_config.rb index 468e66a7..af989a25 100644 --- a/lib/secure_headers/utils/cookies_config.rb +++ b/lib/secure_headers/utils/cookies_config.rb @@ -65,7 +65,7 @@ def validate_samesite_hash_config! def validate_hash_or_true_or_opt_out!(attribute) if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute])) - raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") + raise CookiesConfigError.new("#{attribute} cookie config must be a hash, true, or SecureHeaders::OPT_OUT") end end From b208b8a671a428e2681e50f63f52ae86d9d0bdb9 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 20 Jul 2018 07:11:15 -1000 Subject: [PATCH 507/636] remove hpkp Fixes https://github.com/twitter/secure_headers/issues/368 --- lib/secure_headers.rb | 14 +--- lib/secure_headers/configuration.rb | 9 --- lib/secure_headers/headers/public_key_pins.rb | 81 ------------------- lib/secure_headers/middleware.rb | 6 -- .../headers/public_key_pins_spec.rb | 38 --------- spec/lib/secure_headers/middleware_spec.rb | 19 ----- spec/lib/secure_headers_spec.rb | 35 -------- spec/spec_helper.rb | 1 - 8 files changed, 3 insertions(+), 200 deletions(-) delete mode 100644 lib/secure_headers/headers/public_key_pins.rb delete mode 100644 spec/lib/secure_headers/headers/public_key_pins_spec.rb diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 9926fb2b..6426e538 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "secure_headers/hash_helper" require "secure_headers/headers/cookie" -require "secure_headers/headers/public_key_pins" require "secure_headers/headers/content_security_policy" require "secure_headers/headers/x_frame_options" require "secure_headers/headers/strict_transport_security" @@ -18,8 +17,7 @@ require "singleton" require "secure_headers/configuration" -# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT -# or ":optout_of_protection" as a config value to disable a given header +# Provide SecureHeaders::OPT_OUT as a config value to disable a given header module SecureHeaders class NoOpHeaderConfig include Singleton @@ -51,10 +49,6 @@ def opt_out? HTTPS = "https".freeze CSP = ContentSecurityPolicy - # Headers set on http requests (excludes STS and HPKP) - HTTPS_HEADER_CLASSES = - [StrictTransportSecurity, PublicKeyPins].freeze - class << self # Public: override a given set of directives for the current request. If a # value already exists for a given directive, it will be overridden. @@ -138,7 +132,7 @@ def opt_out_of_all_protection(request) # Public: Builds the hash of headers that should be applied base on the # request. # - # StrictTransportSecurity and PublicKeyPins are not applied to http requests. + # StrictTransportSecurity is not applied to http requests. # See #config_for to determine which config is used for a given request. # # Returns a hash of header names => header values. The value @@ -151,9 +145,7 @@ def header_hash_for(request) headers = config.generate_headers if request.scheme != HTTPS - HTTPS_HEADER_CLASSES.each do |klass| - headers.delete(klass::HEADER_NAME) - end + headers.delete(StrictTransportSecurity::HEADER_NAME) end headers end diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 159e8762..a04a38e2 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -115,7 +115,6 @@ def deep_copy_if_hash(value) expect_certificate_transparency: ExpectCertificateTransparency, csp: ContentSecurityPolicy, csp_report_only: ContentSecurityPolicy, - hpkp: PublicKeyPins, cookies: Cookie, }.freeze @@ -144,7 +143,6 @@ def initialize(&block) @clear_site_data = nil @csp = nil @csp_report_only = nil - @hpkp = nil @hsts = nil @x_content_type_options = nil @x_download_options = nil @@ -153,7 +151,6 @@ def initialize(&block) @x_xss_protection = nil @expect_certificate_transparency = nil - self.hpkp = OPT_OUT self.referrer_policy = OPT_OUT self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT) self.csp_report_only = OPT_OUT @@ -178,7 +175,6 @@ def dup copy.clear_site_data = @clear_site_data copy.expect_certificate_transparency = @expect_certificate_transparency copy.referrer_policy = @referrer_policy - copy.hpkp = @hpkp copy end @@ -263,10 +259,5 @@ def csp_report_only=(new_csp) raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash" end end - - def hpkp_report_host - return nil unless @hpkp && hpkp != OPT_OUT && @hpkp[:report_uri] - URI.parse(@hpkp[:report_uri]).host - end end end diff --git a/lib/secure_headers/headers/public_key_pins.rb b/lib/secure_headers/headers/public_key_pins.rb deleted file mode 100644 index f0850344..00000000 --- a/lib/secure_headers/headers/public_key_pins.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true -module SecureHeaders - class PublicKeyPinsConfigError < StandardError; end - class PublicKeyPins - HEADER_NAME = "Public-Key-Pins".freeze - REPORT_ONLY = "Public-Key-Pins-Report-Only".freeze - HASH_ALGORITHMS = [:sha256].freeze - - - class << self - # Public: make an hpkp header name, value pair - # - # Returns nil if not configured, returns header name and value if configured. - def make_header(config, user_agent = nil) - return if config.nil? || config == OPT_OUT - header = new(config) - [header.name, header.value] - end - - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise PublicKeyPinsConfigError.new("config must be a hash.") unless config.is_a? Hash - - if !config[:max_age] - raise PublicKeyPinsConfigError.new("max-age is a required directive.") - elsif config[:max_age].to_s !~ /\A\d+\z/ - raise PublicKeyPinsConfigError.new("max-age must be a number. - #{config[:max_age]} was supplied.") - elsif config[:pins] && config[:pins].length < 2 - raise PublicKeyPinsConfigError.new("A minimum of 2 pins are required.") - end - end - end - - def initialize(config) - @max_age = config.fetch(:max_age, nil) - @pins = config.fetch(:pins, nil) - @report_uri = config.fetch(:report_uri, nil) - @report_only = !!config.fetch(:report_only, nil) - @include_subdomains = !!config.fetch(:include_subdomains, nil) - end - - def name - if @report_only - REPORT_ONLY - else - HEADER_NAME - end - end - - def value - [ - max_age_directive, - pin_directives, - report_uri_directive, - subdomain_directive - ].compact.join("; ").strip - end - - def pin_directives - return nil if @pins.nil? - @pins.collect do |pin| - pin.map do |token, hash| - "pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token) - end - end.join("; ") - end - - def max_age_directive - "max-age=#{@max_age}" if @max_age - end - - def report_uri_directive - "report-uri=\"#{@report_uri}\"" if @report_uri - end - - def subdomain_directive - @include_subdomains ? "includeSubDomains" : nil - end - end -end diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index ece1a7df..e1c07c5b 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true module SecureHeaders class Middleware - HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166" - def initialize(app) @app = app end @@ -13,10 +11,6 @@ def call(env) status, headers, response = @app.call(env) config = SecureHeaders.config_for(req) - if config.hpkp_report_host == req.host - Kernel.warn(HPKP_SAME_HOST_WARNING) - end - flag_cookies!(headers, override_secure(env, config.cookies)) unless config.cookies == OPT_OUT headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] diff --git a/spec/lib/secure_headers/headers/public_key_pins_spec.rb b/spec/lib/secure_headers/headers/public_key_pins_spec.rb deleted file mode 100644 index 31e661e0..00000000 --- a/spec/lib/secure_headers/headers/public_key_pins_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true -require "spec_helper" - -module SecureHeaders - describe PublicKeyPins do - specify { expect(PublicKeyPins.new(max_age: 1234, report_only: true).name).to eq("Public-Key-Pins-Report-Only") } - specify { expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins") } - - specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } - specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } - specify do - config = { max_age: 1234, pins: [{ sha256: "base64encodedpin1" }, { sha256: "base64encodedpin2" }] } - header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\"" - expect(PublicKeyPins.new(config).value).to eq(header_value) - end - - context "with an invalid configuration" do - it "raises an exception when max-age is not provided" do - expect do - PublicKeyPins.validate_config!(foo: "bar") - end.to raise_error(PublicKeyPinsConfigError) - end - - it "raises an exception with an invalid max-age" do - expect do - PublicKeyPins.validate_config!(max_age: "abc123") - end.to raise_error(PublicKeyPinsConfigError) - end - - it "raises an exception with less than 2 pins" do - expect do - config = { max_age: 1234, pins: [{ sha256: "base64encodedpin" }] } - PublicKeyPins.validate_config!(config) - end.to raise_error(PublicKeyPinsConfigError) - end - end - end -end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index 3725c73e..b3925f85 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -14,25 +14,6 @@ module SecureHeaders Configuration.default end - it "warns if the hpkp report-uri host is the same as the current host" do - report_host = "report-uri.io" - reset_config - Configuration.default do |config| - config.hpkp = { - max_age: 10000000, - pins: [ - {sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"}, - {sha256: "73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f"} - ], - report_uri: "https://#{report_host}/example-hpkp" - } - end - - expect(Kernel).to receive(:warn).with(Middleware::HPKP_SAME_HOST_WARNING) - - middleware.call(Rack::MockRequest.env_for("https://#{report_host}", {})) - end - it "sets the headers" do _, env = middleware.call(Rack::MockRequest.env_for("https://looocalhost", {})) expect_default_values(env) diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 6ef9d05b..be608c74 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -90,16 +90,6 @@ module SecureHeaders Configuration.default do |config| config.csp = { default_src: ["example.com"], script_src: %w('self') } config.csp_report_only = config.csp - config.hpkp = { - report_only: false, - max_age: 10000000, - include_subdomains: true, - report_uri: "https://report-uri.io/example-hpkp", - pins: [ - {sha256: "abc"}, - {sha256: "123"} - ] - } end SecureHeaders.opt_out_of_all_protection(request) hash = SecureHeaders.header_hash_for(request) @@ -141,23 +131,6 @@ module SecureHeaders expect(SecureHeaders.header_hash_for(plaintext_request)[StrictTransportSecurity::HEADER_NAME]).to be_nil end - it "does not set the HPKP header if request is over HTTP" do - plaintext_request = Rack::Request.new({}) - Configuration.default do |config| - config.hpkp = { - max_age: 1_000_000, - include_subdomains: true, - report_uri: "//example.com/uri-directive", - pins: [ - { sha256: "abc" }, - { sha256: "123" } - ] - } - end - - expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil - end - context "content security policy" do let(:chrome_request) { Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome])) @@ -531,14 +504,6 @@ module SecureHeaders end.to raise_error(ReferrerPolicyConfigError) end - it "validates your hpkp config upon configuration" do - expect do - Configuration.default do |config| - config.hpkp = "lol" - end - end.to raise_error(PublicKeyPinsConfigError) - end - it "validates your cookies config upon configuration" do expect do Configuration.default do |config| diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 96f06264..9d002278 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -37,7 +37,6 @@ def expect_default_values(hash) expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil expect(hash[SecureHeaders::ClearSiteData::HEADER_NAME]).to be_nil expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil - expect(hash[SecureHeaders::PublicKeyPins::HEADER_NAME]).to be_nil end module SecureHeaders From 9965178f125a977d1eb248a52003fba5ffa8b321 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 27 Jul 2018 07:01:19 -1000 Subject: [PATCH 508/636] Add note on API configs --- README.md | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 1d4088b9..d7fd536f 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,6 @@ **master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. -**The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](docs/upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. - -**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be not be maintained once 4.x is released**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. - The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) - https://csp.withgoogle.com @@ -33,20 +29,6 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu - [Hashes](docs/hashes.md) - [Sinatra Config](docs/sinatra.md) -## Getting Started - -### Rails 3+ - -For Rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. If for some reason the middleware is not being included follow the instructions for Rails 2. - -### Rails 2 - -For Rails 2 or non-rails applications, an explicit statement is required to use the middleware component. - -```ruby -use SecureHeaders::Middleware -``` - ## Configuration If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. @@ -119,6 +101,23 @@ X-Permitted-Cross-Domain-Policies: none X-Xss-Protection: 1; mode=block ``` +## API configurations + +Which headers you decide to use for API responses is entirely a personal choice. Things like X-Frame-Options seem to have no place in an API response and would be wasting bytes. While this is true, browsers can do funky things with non-html responses. At the minimum, we suggest CSP: + +```ruby +SecureHeaders::Configuration.override(:api) do |config| + config.csp = { default_src: 'none' } + config.hsts = SecureHeaders::OPT_OUT + config.x_frame_options = SecureHeaders::OPT_OUT + config.x_content_type_options = SecureHeaders::OPT_OUT + config.x_xss_protection = SecureHeaders::OPT_OUT + config.x_permitted_cross_domain_policies = SecureHeaders::OPT_OUT +end +``` + +However, I would consider these headers anyways depending on your load and bandwidth requirements. + ## Similar libraries * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers) From 64a4fadebbeed5a43e73d1850fc5eb7ff83c65f1 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 22 Feb 2019 14:21:36 -1000 Subject: [PATCH 509/636] update supported ruby versions --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f3b578c..36e30ce1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,9 @@ language: ruby rvm: - ruby-head + - 2.6.1 - 2.5.0 - 2.4.3 - - 2.3.6 - - 2.2.9 - jruby-head env: From 1670f4ba3b05a04b76a6ef3db4581326d83a3b68 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Fri, 22 Feb 2019 14:41:37 -1000 Subject: [PATCH 510/636] Bump to 6.1.0 and readd VERSION constant --- .ruby-version | 2 +- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 3 +++ secure_headers.gemspec | 6 +++++- 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 lib/secure_headers/version.rb diff --git a/.ruby-version b/.ruby-version index 8e8299dc..6a6a3d8e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.2 +2.6.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 95dc7dd0..979110fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1 + +Adds support for navigate-to, prefetch-src, and require-sri-for #395 + ## 6.0 - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes. diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb new file mode 100644 index 00000000..dabf5bc8 --- /dev/null +++ b/lib/secure_headers/version.rb @@ -0,0 +1,3 @@ +module SecureHeaders + VERSION = "6.1.0" +end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index c73db792..79b519d2 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,8 +1,12 @@ +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "secure_headers/version" + # -*- encoding: utf-8 -*- # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" - gem.version = "6.0.0" + gem.version = SecureHeaders::VERSION gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." From 543e6712aadae08f1653ed973e6b6204f7eac26a Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Wed, 27 Feb 2019 08:34:03 -1000 Subject: [PATCH 511/636] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 979110fc..b0c1c722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Adds support for navigate-to, prefetch-src, and require-sri-for #395 +NOTE: this version is a breaking change due to the removal of HPKP. Remove the HPKP config, the standard is dead. Apologies for not doing a proper deprecate/major rev cycle :pray: + ## 6.0 - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes. From 267663ba8a795fcbea91dc7ea2835147bbbf3880 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 25 Jun 2019 14:21:41 -1000 Subject: [PATCH 512/636] clean up some linter errors showing up in newer CI --- lib/secure_headers/version.rb | 2 ++ secure_headers.gemspec | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index dabf5bc8..5aa530d0 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SecureHeaders VERSION = "6.1.0" end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 79b519d2..c5f7292c 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -1,9 +1,9 @@ +# frozen_string_literal: true + lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "secure_headers/version" -# -*- encoding: utf-8 -*- -# frozen_string_literal: true Gem::Specification.new do |gem| gem.name = "secure_headers" gem.version = SecureHeaders::VERSION From cafeb214e4063aa73e645cb81f0cfea97f4c2afb Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Tue, 25 Jun 2019 16:25:04 -0700 Subject: [PATCH 513/636] Add option to disable appending 'unsafe-inline' when using nonces --- lib/secure_headers/headers/content_security_policy.rb | 3 ++- lib/secure_headers/headers/content_security_policy_config.rb | 1 + lib/secure_headers/headers/policy_management.rb | 3 ++- .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index c744ebce..abac5c5b 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -179,7 +179,8 @@ def populate_nonces(directive, source_list) # unsafe-inline, this is more concise. def append_nonce(source_list, nonce) if nonce - source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE) + source_list.push("'nonce-#{nonce}'") + source_list.push(UNSAFE_INLINE) unless @config[:disable_nonce_backwards_compatibility] end source_list diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 352739d8..505e8069 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -42,6 +42,7 @@ def initialize(hash) @style_src = nil @worker_src = nil @upgrade_insecure_requests = nil + @disable_nonce_backwards_compatibility = nil from_hash(hash) end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index a6e6d4c8..3d97c55a 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -153,7 +153,8 @@ def self.included(base) META_CONFIGS = [ :report_only, - :preserve_schemes + :preserve_schemes, + :disable_nonce_backwards_compatibility ].freeze NONCES = [ diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index f5d38be5..fface8ca 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -145,6 +145,11 @@ module SecureHeaders csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'") end + + it "supports strict-dynamic and opting out of the appended 'unsafe-inline'" do + csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456, disable_nonce_backwards_compatibility: true }) + expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'") + end end end end From 92ac88736ac3f3aad0d6401adb3cc954a1a2989a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Wed, 26 Jun 2019 07:39:39 -1000 Subject: [PATCH 514/636] bump to 6.1.1 --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c1c722..862a2814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.1 + +Adds the ability to disable the automatically-appended `'unsafe-inline'` value when nonces are used #404 (@will) + ## 6.1 Adds support for navigate-to, prefetch-src, and require-sri-for #395 diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 5aa530d0..65f85e28 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.1.0" + VERSION = "6.1.1" end From 08e73886ca55356627c65c1a885f671b60fa25fa Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Mon, 12 Aug 2019 11:44:20 -1000 Subject: [PATCH 515/636] fix travis badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7fd536f..6d6ff115 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) +# Secure Headers [![Build Status](https://travis-ci.org/twitter/secure_headers.svg?branch=master)](http://travis-ci.org/twitter/secure_headers) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) **master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. From b18d3eb11a974556cdcb26e36f49ce3c9dc71316 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Mon, 21 Oct 2019 12:25:14 -0700 Subject: [PATCH 516/636] Update named_overrides_and_appends.md --- docs/named_overrides_and_appends.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/named_overrides_and_appends.md b/docs/named_overrides_and_appends.md index 5cb374c4..e4bd32fd 100644 --- a/docs/named_overrides_and_appends.md +++ b/docs/named_overrides_and_appends.md @@ -76,11 +76,6 @@ class ApplicationController < ActionController::Base SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config| config.csp[:script_src] << "otherdomain.com" end - - # overrides the :script_from_otherdomain_com configuration - SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config| - config.csp[:script_src] << "evenanotherdomain.com" - end end class MyController < ApplicationController From 71ce42e7f410cc3139f2874eef1e8c57e4062b96 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Tue, 22 Oct 2019 14:06:34 -0500 Subject: [PATCH 517/636] Document `disable_nonce_backwards_compatibility` --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6d6ff115..97bc5b8f 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ SecureHeaders::Configuration.default do |config| config.csp = { # "meta" values. these will shape the header, but the values are not included in the header. preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. + disable_nonce_backwards_compatibility: true, # default: false. If false, `unsafe-inline` will be added automatically when using nonces. If true, it won't. See #403 for why you'd want this. # directive values: these values will directly translate into source directives default_src: %w('none'), From 623ac24495e69899c11143c7c79a415dddf778b0 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Jan 2020 12:05:40 -1000 Subject: [PATCH 518/636] Add support for SameSite=None Fixes https://github.com/twitter/secure_headers/issues/412 --- Gemfile | 2 +- lib/secure_headers/headers/cookie.rb | 8 +++- lib/secure_headers/utils/cookies_config.rb | 10 +++-- .../lib/secure_headers/headers/cookie_spec.rb | 39 +++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 39d06141..f20c44d7 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ end group :guard do gem "growl" - gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24] + gem "guard-rspec", platforms: [:ruby] gem "rb-fsevent" gem "terminal-notifier-guard" end diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index f92900da..0ff045c2 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -94,12 +94,14 @@ def samesite_cookie "SameSite=Lax" elsif flag_samesite_strict? "SameSite=Strict" + elsif flag_samesite_none? + "SameSite=None" end end def flag_samesite? return false if config == OPT_OUT || config[:samesite] == OPT_OUT - flag_samesite_lax? || flag_samesite_strict? + flag_samesite_lax? || flag_samesite_strict? || flag_samesite_none? end def flag_samesite_lax? @@ -110,6 +112,10 @@ def flag_samesite_strict? flag_samesite_enforcement?(:strict) end + def flag_samesite_none? + flag_samesite_enforcement?(:none) + end + def flag_samesite_enforcement?(mode) return unless config[:samesite] diff --git a/lib/secure_headers/utils/cookies_config.rb b/lib/secure_headers/utils/cookies_config.rb index af989a25..9c673f2f 100644 --- a/lib/secure_headers/utils/cookies_config.rb +++ b/lib/secure_headers/utils/cookies_config.rb @@ -43,10 +43,12 @@ def validate_samesite_config! # when configuring with booleans, only one enforcement is permitted def validate_samesite_boolean_config! - if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict) - raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") - elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax) - raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") + if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && (config[:samesite].key?(:strict) || config[:samesite].key?(:none)) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax with strict or no enforcement is not permitted.") + elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:none)) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure strict with lax or no enforcement is not permitted.") + elsif config[:samesite].key?(:none) && config[:samesite][:none].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:strict)) + raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure no enforcement with lax or strict is not permitted.") end end diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index db7eb0c1..a7477fe7 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -93,6 +93,21 @@ module SecureHeaders expect(cookie.to_s).to eq("_session=thisisatest") end + it "flags SameSite=None" do + cookie = Cookie.new(raw_cookie, samesite: { none: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=None") + end + + it "flags SameSite=None when configured with a boolean" do + cookie = Cookie.new(raw_cookie, samesite: { none: true}, secure: OPT_OUT, httponly: OPT_OUT) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=None") + end + + it "does not flag cookies as SameSite=none when excluded" do + cookie = Cookie.new(raw_cookie, samesite: { none: { except: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) + expect(cookie.to_s).to eq("_session=thisisatest") + end + it "flags SameSite=Strict when configured with a boolean" do cookie = Cookie.new(raw_cookie, {samesite: { strict: true}, secure: OPT_OUT, httponly: OPT_OUT}) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") @@ -155,6 +170,30 @@ module SecureHeaders end.to raise_error(CookiesConfigError) end + it "raises an exception when SameSite lax and none enforcement modes are configured with booleans" do + expect do + Cookie.validate_config!(samesite: { lax: true, none: true}) + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when SameSite strict and none enforcement modes are configured with booleans" do + expect do + Cookie.validate_config!(samesite: { strict: true, none: true}) + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when SameSite none and lax enforcement modes are configured with booleans" do + expect do + Cookie.validate_config!(samesite: { none: true, lax: true}) + end.to raise_error(CookiesConfigError) + end + + it "raises an exception when SameSite none and strict enforcement modes are configured with booleans" do + expect do + Cookie.validate_config!(samesite: { none: true, strict: true}) + end.to raise_error(CookiesConfigError) + end + it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do expect do Cookie.validate_config!(samesite: { lax: true, strict: { only: ["_anything"] } }) From 0664df0967677a2013988eaa2d2321c0447ea64b Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Jan 2020 12:15:42 -1000 Subject: [PATCH 519/636] docs --- docs/cookies.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/cookies.md b/docs/cookies.md index d136953c..3a1e937a 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -52,13 +52,14 @@ config.cookies = { } ``` -`Strict` and `Lax` enforcement modes can also be specified using a Hash. +`Strict`, `Lax`, and `None` enforcement modes can also be specified using a Hash. ```ruby config.cookies = { samesite: { strict: { only: ['_rails_session'] }, - lax: { only: ['_guest'] } + lax: { only: ['_guest'] }, + none: { only: ['_tracking'] }, } } ``` From a03feadf73dc39ac33868b8a8101c48c8bf76131 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Jan 2020 12:36:12 -1000 Subject: [PATCH 520/636] DRY up tests a little --- .../lib/secure_headers/headers/cookie_spec.rb | 49 +++++-------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index a7477fe7..078a664d 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -68,44 +68,21 @@ module SecureHeaders end context "SameSite cookies" do - it "flags SameSite=Lax" do - cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) - expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") - end - - it "flags SameSite=Lax when configured with a boolean" do - cookie = Cookie.new(raw_cookie, samesite: { lax: true}, secure: OPT_OUT, httponly: OPT_OUT) - expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") - end - - it "does not flag cookies as SameSite=Lax when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) - expect(cookie.to_s).to eq("_session=thisisatest") - end - - it "flags SameSite=Strict" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) - expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") - end - - it "does not flag cookies as SameSite=Strict when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] }}, secure: OPT_OUT, httponly: OPT_OUT) - expect(cookie.to_s).to eq("_session=thisisatest") - end - - it "flags SameSite=None" do - cookie = Cookie.new(raw_cookie, samesite: { none: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) - expect(cookie.to_s).to eq("_session=thisisatest; SameSite=None") - end + %w(None Lax Strict).each do |flag| + it "flags SameSite=#{flag}" do + cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}") + end - it "flags SameSite=None when configured with a boolean" do - cookie = Cookie.new(raw_cookie, samesite: { none: true}, secure: OPT_OUT, httponly: OPT_OUT) - expect(cookie.to_s).to eq("_session=thisisatest; SameSite=None") - end + it "flags SameSite=#{flag} when configured with a boolean" do + cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => true}, secure: OPT_OUT, httponly: OPT_OUT) + expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}") + end - it "does not flag cookies as SameSite=none when excluded" do - cookie = Cookie.new(raw_cookie, samesite: { none: { except: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) - expect(cookie.to_s).to eq("_session=thisisatest") + it "does not flag cookies as SameSite=#{flag} when excluded" do + cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => { except: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) + expect(cookie.to_s).to eq("_session=thisisatest") + end end it "flags SameSite=Strict when configured with a boolean" do From 896c36dbea483c94f672d594464bfdf3bcdb247a Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Jan 2020 12:41:03 -1000 Subject: [PATCH 521/636] dry up more tests --- .../lib/secure_headers/headers/cookie_spec.rb | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index 078a664d..cdb24246 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -141,34 +141,15 @@ module SecureHeaders end.to raise_error(CookiesConfigError) end - it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do - expect do - Cookie.validate_config!(samesite: { lax: true, strict: true}) - end.to raise_error(CookiesConfigError) - end - - it "raises an exception when SameSite lax and none enforcement modes are configured with booleans" do - expect do - Cookie.validate_config!(samesite: { lax: true, none: true}) - end.to raise_error(CookiesConfigError) - end - - it "raises an exception when SameSite strict and none enforcement modes are configured with booleans" do - expect do - Cookie.validate_config!(samesite: { strict: true, none: true}) - end.to raise_error(CookiesConfigError) - end - - it "raises an exception when SameSite none and lax enforcement modes are configured with booleans" do - expect do - Cookie.validate_config!(samesite: { none: true, lax: true}) - end.to raise_error(CookiesConfigError) - end - - it "raises an exception when SameSite none and strict enforcement modes are configured with booleans" do - expect do - Cookie.validate_config!(samesite: { none: true, strict: true}) - end.to raise_error(CookiesConfigError) + cookie_options = %i(none lax strict) + cookie_options.each do |flag| + (cookie_options - [flag]).each do |other_flag| + it "raises an exception when SameSite #{flag} and #{other_flag} enforcement modes are configured with booleans" do + expect do + Cookie.validate_config!(samesite: { flag => true, other_flag => true}) + end.to raise_error(CookiesConfigError) + end + end end it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do From d77456ff999d58ff364db065966c2824893aac82 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Jan 2020 15:44:53 -1000 Subject: [PATCH 522/636] pin rubocop to 'legacy' rubocop-github https://github.com/github/rubocop-github#legacy-usage --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index f20c44d7..ce7bde63 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ group :test do gem "pry-nav" gem "rack" gem "rspec" - gem "rubocop" + gem "rubocop", "< 0.68" gem "rubocop-github" gem "term-ansicolor" gem "tins" From 47c8be9454ece9eeb20b1c97cf3b8482c0db8089 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Jan 2020 15:54:45 -1000 Subject: [PATCH 523/636] let's get some more modern ruby while we're at it --- .ruby-version | 2 +- .travis.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ruby-version b/.ruby-version index 6a6a3d8e..24ba9a38 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.1 +2.7.0 diff --git a/.travis.yml b/.travis.yml index 36e30ce1..d8497962 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,9 @@ language: ruby rvm: - ruby-head - - 2.6.1 - - 2.5.0 - - 2.4.3 + - 2.5 + - 2.6 + - 2.7 - jruby-head env: From 17a59584fdd3c06c343577335c479986a3f5ee50 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Jan 2020 15:55:19 -1000 Subject: [PATCH 524/636] ok, maybe not that recent --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 24ba9a38..57cf282e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.0 +2.6.5 From 0d1eb1b02f6d13be562d552f33e3af468748d0e7 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Jan 2020 17:27:07 -1000 Subject: [PATCH 525/636] version bump for SameSite=none --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 862a2814..2f5912aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.2 + +Adds the ability to specify `SameSite=none` with the same configurability as `Strict`/`Lax` in order to disable Chrome's soon-to-be-lax-by-default state. + ## 6.1.1 Adds the ability to disable the automatically-appended `'unsafe-inline'` value when nonces are used #404 (@will) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 65f85e28..0fa33da0 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.1.1" + VERSION = "6.1.2" end From 0169dd80fd041d08ad0d10e2461a486ba55bec52 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Fri, 10 Jan 2020 06:19:21 -1000 Subject: [PATCH 526/636] Add some examples to the cookie docs to more closely reflect how a deployment would look --- docs/cookies.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cookies.md b/docs/cookies.md index 3a1e937a..79f88cab 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -57,9 +57,9 @@ config.cookies = { ```ruby config.cookies = { samesite: { - strict: { only: ['_rails_session'] }, - lax: { only: ['_guest'] }, - none: { only: ['_tracking'] }, + strict: { only: ['_rails_session_duplicate'] }, + lax: { only: ['_guest', '_rails_session', 'device_id'] }, + none: { only: ['_tracking', 'saml_cookie'] }, } } ``` From c73952a3185472c892d556956774d4e54e3bdb33 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Fri, 10 Jan 2020 06:20:42 -1000 Subject: [PATCH 527/636] Actually, the session ID stuff wasn't quite accurate The reason for `none` and `duplicate` is so you can find the differences. Setting it to lax would break 3rd party interactions. --- docs/cookies.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cookies.md b/docs/cookies.md index 79f88cab..92a671a3 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -57,9 +57,9 @@ config.cookies = { ```ruby config.cookies = { samesite: { - strict: { only: ['_rails_session_duplicate'] }, + strict: { only: ['session_id_duplicate'] }, lax: { only: ['_guest', '_rails_session', 'device_id'] }, - none: { only: ['_tracking', 'saml_cookie'] }, + none: { only: ['_tracking', 'saml_cookie', 'session_id'] }, } } ``` From ffd593cf5713faa99f10c6e7d3a5844832aaa049 Mon Sep 17 00:00:00 2001 From: Juanito Fatas Date: Sat, 11 Jan 2020 12:42:22 +0900 Subject: [PATCH 528/636] Fix references to OPT_OUT constant --- docs/cookies.md | 2 +- docs/upgrading-to-6-0.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cookies.md b/docs/cookies.md index 92a671a3..f22971ae 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -25,7 +25,7 @@ Boolean-based configuration is intended to globally enable or disable a specific ```ruby config.cookies = { secure: true, # mark all cookies as Secure - httponly: OPT_OUT, # do not mark any cookies as HttpOnly + httponly: SecureHeaders::OPT_OUT, # do not mark any cookies as HttpOnly } ``` diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index cb92effd..f58950ac 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -5,7 +5,7 @@ The original implementation of name overrides worked by making a copy of the def ```ruby class ApplicationController < ActionController::Base Configuration.default do |config| - config.x_frame_options = OPT_OUT + config.x_frame_options = SecureHeaders::OPT_OUT end SecureHeaders::Configuration.override(:dynamic_override) do |config| From 902041bab6b3e7c29644f49d6dd0ef75b9c5bbb0 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Tue, 21 Jan 2020 07:28:21 -1000 Subject: [PATCH 529/636] Do years even matter? --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 875f997d..ba76475e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2013, 2014, 2015, 2016, 2017 Twitter, Inc. +Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Twitter, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From 86c762aea480d0a776246652586f93e026f6799f Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Tue, 21 Jan 2020 07:28:51 -1000 Subject: [PATCH 530/636] Remove outdated APL license blurb from readme, use only the LICENSE file Fixes https://github.com/twitter/secure_headers/issues/415 --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 97bc5b8f..d044dd1e 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,3 @@ However, I would consider these headers anyways depending on your load and bandw * Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security) * Ember.js [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/) * PHP [secure-headers](https://github.com/BePsvPT/secure-headers) - -## License - -Copyright 2013-2014 Twitter, Inc and other contributors. - -Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 From 2068ba7bb63fb98786db828091cb52304bcae560 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 21 Jan 2020 08:44:43 -1000 Subject: [PATCH 531/636] clean up some warnings --- lib/secure_headers/configuration.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index a04a38e2..7c719249 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -126,7 +126,9 @@ def deep_copy_if_hash(value) # The list of attributes that must respond to a `make_header` method HEADERABLE_ATTRIBUTES = (CONFIG_ATTRIBUTES - [:cookies]).freeze - attr_accessor(*CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys) + attr_writer(*(CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.reject { |key| [:csp, :csp_report_only].include?(key)}.keys)) + + attr_reader(*(CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys)) @script_hashes = nil @style_hashes = nil From 3c4b86edd6745275da22d92290872da202d73e64 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 21 Jan 2020 08:44:58 -1000 Subject: [PATCH 532/636] escape semicolons by replacing them with spaces See https://github.com/twitter/secure_headers/issues/418 --- lib/secure_headers/headers/content_security_policy.rb | 11 ++++++++--- .../headers/content_security_policy_spec.rb | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index abac5c5b..9f50623b 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -103,10 +103,15 @@ def build_media_type_list_directive(directive) # Returns a string representing a directive. def build_source_list_directive(directive) source_list = @config.directive_value(directive) - if source_list != OPT_OUT && source_list && source_list.any? - normalized_source_list = minify_source_list(directive, source_list) - [symbol_to_hyphen_case(directive), normalized_source_list].join(" ") + minified_source_list = minify_source_list(directive, source_list).join(" ") + + if minified_source_list.include?(";") + Kernel.warn("#{directive} contains a ; in '#{minified_source_list}' which will raise an error in future versions. It has been replaced with a blank space.") + end + + escaped_source_list = minified_source_list.gsub(";", " ") + [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index fface8ca..32b9b44d 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -28,6 +28,11 @@ module SecureHeaders expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:") end + it "deprecates and escapes semicolons in directive source lists" do + expect(Kernel).to receive(:warn).with("frame_ancestors contains a ; in 'google.com;script-src *;.;' which will raise an error in future versions. It has been replaced with a blank space.") + expect(ContentSecurityPolicy.new(frame_ancestors: %w(https://google.com;script-src https://*;.;)).value).to eq("frame-ancestors google.com script-src * .") + end + it "discards 'none' values if any other source expressions are present" do csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none'))) expect(csp.value).not_to include("'none'") From eed6c1606feaa874ba53b2ba0e2405accd8d1105 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 21 Jan 2020 09:02:20 -1000 Subject: [PATCH 533/636] lint --- lib/secure_headers/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 7c719249..26c3a0c3 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -126,7 +126,7 @@ def deep_copy_if_hash(value) # The list of attributes that must respond to a `make_header` method HEADERABLE_ATTRIBUTES = (CONFIG_ATTRIBUTES - [:cookies]).freeze - attr_writer(*(CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.reject { |key| [:csp, :csp_report_only].include?(key)}.keys)) + attr_writer(*(CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.reject { |key| [:csp, :csp_report_only].include?(key) }.keys)) attr_reader(*(CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys)) From 1298905068931621a2c1988b175a1da186bcd641 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 21 Jan 2020 10:42:02 -1000 Subject: [PATCH 534/636] bump to 6.2 --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5912aa..32cb29aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.2.0 + +Fixes semicolon injection issue reported by @mvgijssel see https://github.com/twitter/secure_headers/issues/418 + ## 6.1.2 Adds the ability to specify `SameSite=none` with the same configurability as `Strict`/`Lax` in order to disable Chrome's soon-to-be-lax-by-default state. diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 0fa33da0..1bbd57e7 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.1.2" + VERSION = "6.2.0" end From 3a2b548223de854ab9768ae07acedfcd2ac211e3 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 21 Jan 2020 12:32:41 -1000 Subject: [PATCH 535/636] Filter and warn on newlines --- lib/secure_headers/headers/content_security_policy.rb | 6 +++--- .../secure_headers/headers/content_security_policy_spec.rb | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 9f50623b..cb75050c 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -106,11 +106,11 @@ def build_source_list_directive(directive) if source_list != OPT_OUT && source_list && source_list.any? minified_source_list = minify_source_list(directive, source_list).join(" ") - if minified_source_list.include?(";") - Kernel.warn("#{directive} contains a ; in '#{minified_source_list}' which will raise an error in future versions. It has been replaced with a blank space.") + if minified_source_list =~ /(\n|;)/ + Kernel.warn("#{directive} contains a #{$1} in #{minified_source_list.inspect} which will raise an error in future versions. It has been replaced with a blank space.") end - escaped_source_list = minified_source_list.gsub(";", " ") + escaped_source_list = minified_source_list.gsub(/[\n;]/, " ") [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 32b9b44d..c1f24e6c 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -29,10 +29,15 @@ module SecureHeaders end it "deprecates and escapes semicolons in directive source lists" do - expect(Kernel).to receive(:warn).with("frame_ancestors contains a ; in 'google.com;script-src *;.;' which will raise an error in future versions. It has been replaced with a blank space.") + expect(Kernel).to receive(:warn).with(%(frame_ancestors contains a ; in "google.com;script-src *;.;" which will raise an error in future versions. It has been replaced with a blank space.)) expect(ContentSecurityPolicy.new(frame_ancestors: %w(https://google.com;script-src https://*;.;)).value).to eq("frame-ancestors google.com script-src * .") end + it "deprecates and escapes semicolons in directive source lists" do + expect(Kernel).to receive(:warn).with(%(frame_ancestors contains a \n in "\\nfoo.com\\nhacked" which will raise an error in future versions. It has been replaced with a blank space.)) + expect(ContentSecurityPolicy.new(frame_ancestors: ["\nfoo.com\nhacked"]).value).to eq("frame-ancestors foo.com hacked") + end + it "discards 'none' values if any other source expressions are present" do csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none'))) expect(csp.value).not_to include("'none'") From 722a69051acce9d26ab0d0648fe10fd2ff77baa8 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 21 Jan 2020 13:05:37 -1000 Subject: [PATCH 536/636] bump to 6.3 --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32cb29aa..dfb70da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.3.0 + +Fixes newline injection issue + ## 6.2.0 Fixes semicolon injection issue reported by @mvgijssel see https://github.com/twitter/secure_headers/issues/418 diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 1bbd57e7..235ce18b 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.2.0" + VERSION = "6.3.0" end From 65517299dc011f26747797dd4c219ac653a224bf Mon Sep 17 00:00:00 2001 From: Kelly Kaoudis Date: Thu, 23 Jan 2020 14:38:27 -0700 Subject: [PATCH 537/636] add Twitter and early contributors up to 2015 or so to README --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d044dd1e..127d3147 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,47 @@ SecureHeaders::Configuration.override(:api) do |config| end ``` -However, I would consider these headers anyways depending on your load and bandwidth requirements. +However, I would consider these headers anyways depending on your load and bandwidth requirements. + +## Acknowledgements + +This project originated within the Security team at Twitter, and its contributors include + +* Neil Matatall @oreoshake +* Chris Aniszczyk +* Artur Dryomov +* Bjørn Mæland +* Arthur Chiu +* Jonathan Viney +* Jeffrey Horn +* David Collazo +* Brendon Murphy +* William Makley +* Reed Loden +* Noah Kantrowitz +* Wyatt Anderson +* Salimane Adjao Moustapha +* Francois Chagnon +* Jeff Hodges +* Ian Melven +* Darío Javier Cravero +* Logan Hasson +* Raul E Rangel +* Steve Agalloco +* Nate Collings +* Josh Kalderimis +* Alex Kwiatkowski +* Julich Mera +* Jesse Storimer +* Tom Daniels +* Kolja Dummann +* Jean-Philippe Doyle +* Blake Hitchcock +* vanderhoorn +* orthographic-pedant +* Narsimham Chelluri + +If you've made a contribution and see your name missing from the list, make a PR and add it! ## Similar libraries From 6d8fca4b4fe791576d13b3bd6d2a53f0796dddc1 Mon Sep 17 00:00:00 2001 From: Kelly Kaoudis Date: Thu, 13 Feb 2020 15:25:50 -0700 Subject: [PATCH 538/636] Add twitter-archive fork reference (#427) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 127d3147..c2466b61 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,9 @@ However, I would consider these headers anyways depending on your load and bandw ## Acknowledgements -This project originated within the Security team at Twitter, and its contributors include +This project originated within the Security team at Twitter. An archived fork from the point of transition is here: https://github.com/twitter-archive/secure_headers. +Contributors include: * Neil Matatall @oreoshake * Chris Aniszczyk * Artur Dryomov From cbf964db0fc4e6542a83126c2c5664c886c0824c Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Fri, 14 Feb 2020 23:09:05 -1000 Subject: [PATCH 539/636] Add GitHub actions CI setup (#428) Use GitHub actions instead of Travis for CI. This removes coverage for jruby and ruby head in favor of the well-supported versions --- .github/workflows/build.yml | 23 +++++++++++++++++++++++ .travis.yml | 28 ---------------------------- 2 files changed, 23 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..35dcdc9a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: Build + Test +on: [push] + +jobs: + build: + name: Build + Test + runs-on: ubuntu-latest + strategy: + matrix: + ruby: [ '2.4', '2.5', '2.6' ] + + steps: + - uses: actions/checkout@master + - name: Set up Ruby ${{ matrix.ruby }} + uses: actions/setup-ruby@v1 + with: + version: ${{ matrix.ruby }} + - name: Build and test with Rake + run: | + gem install bundler + bundle install --jobs 4 --retry 3 --without guard + bundle exec rspec spec + bundle exec rubocop diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d8497962..00000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: ruby - -rvm: - - ruby-head - - 2.5 - - 2.6 - - 2.7 - - jruby-head - -env: - - SUITE=rspec spec - - SUITE=rubocop - -script: bundle exec $SUITE - -matrix: - allow_failures: - - rvm: jruby-head - - rvm: ruby-head - -before_install: - - gem update --system - - gem --version - - gem update bundler -bundler_args: --without guard -j 3 - -sudo: false -cache: bundler From d6cbf1a9819ec0a5b68319f07cfc1cb455055009 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Thu, 20 Feb 2020 09:18:15 -1000 Subject: [PATCH 540/636] Fix "Input 'version' has been deprecated with message: The version property will not be supported after October 1, 2019. Use ruby-version instead" deprecation message (#429) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35dcdc9a..03b4de65 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Ruby ${{ matrix.ruby }} uses: actions/setup-ruby@v1 with: - version: ${{ matrix.ruby }} + ruby-version: ${{ matrix.ruby }} - name: Build and test with Rake run: | gem install bundler From c3e47ee04e213973e100e24cb5394396623193a2 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Thu, 20 Feb 2020 09:26:20 -1000 Subject: [PATCH 541/636] Add rubocop-performance gem and config to fix deprecation message (#430) * Add rubocop-performance gem and config to fix deprecation message * ehhh apparently the order of the gemfile matters? Gemfile:14:3: C: Bundler/OrderedGems: Gems should be sorted in an alphabetical order within their section of the Gemfile. Gem rubocop-github should appear before rubocop-performance. gem "rubocop-github" ^^^^^^^^^^^^^^^^^^^^ --- .rubocop.yml | 1 + Gemfile | 1 + 2 files changed, 2 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index c299de23..938b22da 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,4 @@ inherit_gem: rubocop-github: - config/default.yml +require: rubocop-performance diff --git a/Gemfile b/Gemfile index ce7bde63..a6b3e54d 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,7 @@ group :test do gem "rspec" gem "rubocop", "< 0.68" gem "rubocop-github" + gem "rubocop-performance" gem "term-ansicolor" gem "tins" end From 2c5676f54e901fed0742f83dbc6e5cc8dd963d85 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Thu, 20 Feb 2020 14:27:16 -1000 Subject: [PATCH 542/636] replace travis badge with actions badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2466b61..34d63967 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Secure Headers [![Build Status](https://travis-ci.org/twitter/secure_headers.svg?branch=master)](http://travis-ci.org/twitter/secure_headers) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) +# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=master) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) **master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. From d3fe69cfaaafc47121fc86de7b3a9330399af870 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Thu, 26 Mar 2020 09:31:10 -1000 Subject: [PATCH 543/636] Run actions on pull_request, not push --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03b4de65..766c6ffb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ name: Build + Test -on: [push] +on: [pull_request] jobs: build: From 38a59ceb654f54b85397da53673e3edaa221dc4e Mon Sep 17 00:00:00 2001 From: Jobert Abma Date: Thu, 26 Mar 2020 12:38:47 -0700 Subject: [PATCH 544/636] Raise on override defined config (#436) * Raise exception when trying to override an existing config * Raise exception when trying to set a named append more than once * Update documentation * Raise an exception when a named append or override with the same name exists Co-authored-by: Neil Matatall <448516+oreoshake@users.noreply.github.com> --- docs/named_overrides_and_appends.md | 4 +- lib/secure_headers/configuration.rb | 11 ++++ spec/lib/secure_headers/configuration_spec.rb | 54 +++++++++++++++++++ spec/spec_helper.rb | 10 ++++ 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/docs/named_overrides_and_appends.md b/docs/named_overrides_and_appends.md index e4bd32fd..4cf4edfd 100644 --- a/docs/named_overrides_and_appends.md +++ b/docs/named_overrides_and_appends.md @@ -1,6 +1,6 @@ ## Named Appends -Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. +Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. Reusing a configuration name is not allowed and will throw an exception. ```ruby def show @@ -91,6 +91,8 @@ class MyController < ApplicationController end ``` +Reusing a configuration name is not allowed and will throw an exception. + By default, a no-op configuration is provided. No headers will be set when this default override is used. ```ruby diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 26c3a0c3..538365a3 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -43,6 +43,9 @@ def default(&block) def override(name, &block) @overrides ||= {} raise "Provide a configuration block" unless block_given? + if named_append_or_override_exists?(name) + raise AlreadyConfiguredError, "Configuration already exists" + end @overrides[name] = block end @@ -59,6 +62,9 @@ def named_appends(name) def named_append(name, &block) @appends ||= {} raise "Provide a configuration block" unless block_given? + if named_append_or_override_exists?(name) + raise AlreadyConfiguredError, "Configuration already exists" + end @appends[name] = block end @@ -68,6 +74,11 @@ def dup private + def named_append_or_override_exists?(name) + (defined?(@appends) && @appends.key?(name)) || + (defined?(@overrides) && @overrides.key?(name)) + end + # Public: perform a basic deep dup. The shallow copy provided by dup/clone # can lead to modifying parent objects. def deep_copy(config) diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 05fd253b..73e9b16b 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -34,6 +34,60 @@ module SecureHeaders expect(Configuration.overrides(:test_override)).to_not be_nil end + describe "#override" do + it "raises on configuring an existing override" do + set_override = Proc.new { + Configuration.override(:test_override) do |config| + config.x_frame_options = "DENY" + end + } + + set_override.call + + expect { set_override.call } + .to raise_error(Configuration::AlreadyConfiguredError, "Configuration already exists") + end + + it "raises when a named append with the given name exists" do + Configuration.named_append(:test_override) do |config| + config.x_frame_options = "DENY" + end + + expect do + Configuration.override(:test_override) do |config| + config.x_frame_options = "SAMEORIGIN" + end + end.to raise_error(Configuration::AlreadyConfiguredError, "Configuration already exists") + end + end + + describe "#named_append" do + it "raises on configuring an existing append" do + set_override = Proc.new { + Configuration.named_append(:test_override) do |config| + config.x_frame_options = "DENY" + end + } + + set_override.call + + expect { set_override.call } + .to raise_error(Configuration::AlreadyConfiguredError, "Configuration already exists") + end + + it "raises when an override with the given name exists" do + Configuration.override(:test_override) do |config| + config.x_frame_options = "DENY" + end + + expect do + Configuration.named_append(:test_override) do |config| + config.x_frame_options = "SAMEORIGIN" + end + end.to raise_error(Configuration::AlreadyConfiguredError, "Configuration already exists") + end + end + it "deprecates the secure_cookies configuration" do expect { Configuration.default do |config| diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9d002278..b0c774d9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -45,10 +45,20 @@ class << self def clear_default_config remove_instance_variable(:@default_config) if defined?(@default_config) end + + def clear_overrides + remove_instance_variable(:@overrides) if defined?(@overrides) + end + + def clear_appends + remove_instance_variable(:@appends) if defined?(@appends) + end end end end def reset_config SecureHeaders::Configuration.clear_default_config + SecureHeaders::Configuration.clear_overrides + SecureHeaders::Configuration.clear_appends end From fa4461f21a231124ab654b09ebe324c3a367d25f Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 8 Jun 2020 13:58:46 -1000 Subject: [PATCH 545/636] replace references to 'master' with 'main' --- .github/workflows/build.yml | 9 +++++---- .github/workflows/sync.yml | 20 ++++++++++++++++++++ CHANGELOG.md | 2 +- README.md | 4 ++-- 4 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/sync.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 766c6ffb..cfaecb7c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ name: Build + Test -on: [pull_request] +on: [pull_request, push] jobs: build: @@ -10,14 +10,15 @@ jobs: ruby: [ '2.4', '2.5', '2.6' ] steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Set up Ruby ${{ matrix.ruby }} uses: actions/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby }} + ruby-version: ${{ matrix.ruby }} - name: Build and test with Rake run: | gem install bundler bundle install --jobs 4 --retry 3 --without guard bundle exec rspec spec - bundle exec rubocop + bundle exec rubocop + diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 00000000..ba8f5f96 --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,20 @@ +# This workflow ensures the "master" branch is always up-to-date with the +# "main" branch (our default one) +name: sync_main_branch +on: + push: + branches: [ main ] +jobs: + catch_up: + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Merge development into master, then push it + run: | + git pull + git checkout master + git merge development + git push diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb70da0..19c25282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -372,7 +372,7 @@ Adds `upgrade-insecure-requests` support for requests from Firefox and Chrome (a ## 3.0.0 -secure_headers 3.0.0 is a near-complete, not-entirely-backward-compatible rewrite. Please see the [upgrade guide](https://github.com/twitter/secureheaders/blob/master/docs/upgrading-to-3-0.md) for an in-depth explanation of the changes and the suggested upgrade path. +secure_headers 3.0.0 is a near-complete, not-entirely-backward-compatible rewrite. Please see the [upgrade guide](https://github.com/twitter/secureheaders/blob/main/docs/upgrading-to-3-0.md) for an in-depth explanation of the changes and the suggested upgrade path. ## 2.5.1 - 2016-02-16 18:11:11 UTC - Remove noisy deprecation warning diff --git a/README.md b/README.md index 34d63967..ba4e6afd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=master) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) +# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=main) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) -**master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. +**main branch represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) From c13b774148e55428564678d4568798ae996132ed Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Mon, 8 Jun 2020 14:15:06 -1000 Subject: [PATCH 546/636] copy/pasta --- .github/workflows/sync.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index ba8f5f96..319e8470 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -16,5 +16,5 @@ jobs: run: | git pull git checkout master - git merge development + git merge main git push From f168403b3af36034828d28f914900e058b8b2abf Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Mon, 8 Jun 2020 14:31:13 -1000 Subject: [PATCH 547/636] Update badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba4e6afd..173c4b3d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=main) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) +# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=main) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/github/secure_headers) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/github/secure_headers) **main branch represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. From 8d2c23c63d489b592bdaee5ce1be71744f61ed96 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Mon, 8 Jun 2020 14:33:53 -1000 Subject: [PATCH 548/636] Remove badges :-/ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 173c4b3d..27cc119e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=main) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/github/secure_headers) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/github/secure_headers) +# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=main) **main branch represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. From 3815ab4da047fb52be9c1c457e08d351c9065d5b Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Thu, 25 Jun 2020 17:05:42 -1000 Subject: [PATCH 549/636] Fix ruby 2.7 deprecation warnings with ** (#443) * Fix ruby 2.7 deprecation warnings with ** * dev in 2.6, test in 2.7 Co-authored-by: Neil Matatall --- .github/workflows/build.yml | 4 ++-- .ruby-version | 2 +- lib/secure_headers/view_helper.rb | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfaecb7c..dcd9cd00 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ name: Build + Test -on: [pull_request, push] +on: [pull_request] jobs: build: @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: [ '2.4', '2.5', '2.6' ] + ruby: [ '2.4', '2.5', '2.6', '2.7' ] steps: - uses: actions/checkout@v2 diff --git a/.ruby-version b/.ruby-version index 57cf282e..338a5b5d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.5 +2.6.6 diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 89344231..91eef4d5 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -21,7 +21,7 @@ def nonced_style_tag(content_or_options = {}, &block) def nonced_stylesheet_link_tag(*args, &block) opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style)) - stylesheet_link_tag(*args, opts, &block) + stylesheet_link_tag(*args, **opts, &block) end # Public: create a script tag using the content security policy nonce. @@ -39,7 +39,7 @@ def nonced_javascript_tag(content_or_options = {}, &block) def nonced_javascript_include_tag(*args, &block) opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script)) - javascript_include_tag(*args, opts, &block) + javascript_include_tag(*args, **opts, &block) end # Public: create a script Webpacker pack tag using the content security policy nonce. @@ -49,7 +49,7 @@ def nonced_javascript_include_tag(*args, &block) def nonced_javascript_pack_tag(*args, &block) opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script)) - javascript_pack_tag(*args, opts, &block) + javascript_pack_tag(*args, **opts, &block) end # Public: create a stylesheet Webpacker link tag using the content security policy nonce. @@ -59,7 +59,7 @@ def nonced_javascript_pack_tag(*args, &block) def nonced_stylesheet_pack_tag(*args, &block) opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style)) - stylesheet_pack_tag(*args, opts, &block) + stylesheet_pack_tag(*args, **opts, &block) end # Public: use the content security policy nonce for this request directly. From f3d3f9d6b09c925ba75aa1d7fea1978b7dbe14ce Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 25 Jun 2020 17:06:26 -1000 Subject: [PATCH 550/636] bump to 6.3.1 --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c25282..64d85376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.3.1 + +Fixes deprecation warnings when running under ruby 2.7 + ## 6.3.0 Fixes newline injection issue diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 235ce18b..c8090fc9 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.3.0" + VERSION = "6.3.1" end From ffec182a2da8fc0d245d99e7f9cbd1c9da1a5e95 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Mon, 21 Sep 2020 19:22:49 -0300 Subject: [PATCH 551/636] Minor improvements to 6.0 upgrade doc [ci skip] (#446) * Fix typo * Improve header description about default configuration * Titlecase "Ruby" --- docs/upgrading-to-6-0.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/upgrading-to-6-0.md b/docs/upgrading-to-6-0.md index f58950ac..c4c249d2 100644 --- a/docs/upgrading-to-6-0.md +++ b/docs/upgrading-to-6-0.md @@ -29,7 +29,7 @@ Prior to 6.0.0, the response would NOT include a `X-Frame-Options` header since ## `ContentSecurityPolicyConfig#merge` and `ContentSecurityPolicyReportOnlyConfig#merge` work more like `Hash#merge` -These classes are typically not directly instantiated by users of SecureHeaders. But, if you access `config.csp` you end up accessing one of these objects. Prior to 6.0.0, `#merge` worked more like `#append` in that it would combine policies (i.e. if both policies contained the same key the values would be combined rather than overwritten). This was not consistent with `#merge!`, which worked more like ruby's `Hash#merge!` (overwriting duplicate keys). As of 6.0.0, `#merge` works the same as `#merge!`, but returns a new object instead of mutating `self`. +These classes are typically not directly instantiated by users of SecureHeaders. But, if you access `config.csp` you end up accessing one of these objects. Prior to 6.0.0, `#merge` worked more like `#append` in that it would combine policies (i.e. if both policies contained the same key the values would be combined rather than overwritten). This was not consistent with `#merge!`, which worked more like Ruby's `Hash#merge!` (overwriting duplicate keys). As of 6.0.0, `#merge` works the same as `#merge!`, but returns a new object instead of mutating `self`. ## `Configuration#get` has been removed @@ -39,7 +39,7 @@ This method is not typically directly called by users of SecureHeaders. Given th Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performance problem and will help to eliminate a class of bugs entirely. -## Configuration the default configuration more than once will result in an Exception +## Calling the default configuration more than once will result in an Exception Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once. @@ -47,4 +47,4 @@ Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` The policy configured is the policy that is delivered in terms of which directives are sent. We still dedup, strip schemes, and look for other optimizations but we will not e.g. conditionally send `frame-src` / `child-src` or apply `nonce`s / `unsafe-inline`. -The primary reason for these per-browser customization was to reduce console warnings. This has lead to many bugs and results inc confusing behavior. Also, console logs are incredibly noisy today and increasingly warn you about perfectly valid things (like sending `X-Frame-Options` and `frame-ancestors` together). +The primary reason for these per-browser customization was to reduce console warnings. This has lead to many bugs and results in confusing behavior. Also, console logs are incredibly noisy today and increasingly warn you about perfectly valid things (like sending `X-Frame-Options` and `frame-ancestors` together). From e7c56e649c364417a028a1a056766bc4c9fc0516 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 23 Dec 2020 13:07:53 +0000 Subject: [PATCH 552/636] Deadlink in readme file in this repo to https://github.com/sourceclear/headlines which does not exist - Status code [404:NotFound] In this repo's readme there is a link to: https://github.com/sourceclear/headlines This is in section for "Similar libraries". However that is a deadlink and gives Status code [404:NotFound]. Looking at https://github.com/sourceclear there does not appear to be any similar named repos belonging to them. So I assume this is not a simple typo and that this repo has been removed. Perhaps the simplest thing to do here is to just remove this "Similar libraries" reference all together? So that is what I have done in this PR. However please feel free to suggest an alternate approach. ## extra I recently created a tool that makes use of the GitHub api to find repos that match certain parameters. Then this tool checks the repo's readme files for dead links: http://githubreadmechecker.com/Home/Search?SingleRepoUri=https%3a%2f%2fgithub.com%2fgithub%2fsecure_headers It can be used to re-check this repo via this url: http://githubreadmechecker.com/Home/Search?SingleRepoUri=https%3a%2f%2fgithub.com%2fgithub%2fsecure_headers I'd love to hear some feedback on this project so please feel free to to share some thoughts, or even start a discussion on the repo's main GitHub page. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 27cc119e..e421a0ba 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,6 @@ If you've made a contribution and see your name missing from the list, make a PR * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers) * Node.js (express) [helmet](https://github.com/helmetjs/helmet) and [hood](https://github.com/seanmonstar/hood) * Node.js (hapi) [blankie](https://github.com/nlf/blankie) -* J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines) * ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) * Go - [secureheader](https://github.com/kr/secureheader) From 722f9c883d9b94dd3e2b30f36e95e1c0f7bf80c8 Mon Sep 17 00:00:00 2001 From: Guille Date: Fri, 5 Feb 2021 11:11:18 +0100 Subject: [PATCH 553/636] Change: add missing directives from CSP version 3: - `script_src_elem` - `script_src_attr` - `style_src_elem` - `style_src_attr` --- .../headers/content_security_policy_config.rb | 4 ++++ lib/secure_headers/headers/policy_management.rb | 14 +++++++++++++- .../headers/policy_management_spec.rb | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 505e8069..3c477161 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -38,8 +38,12 @@ def initialize(hash) @sandbox = nil @script_nonce = nil @script_src = nil + @script_src_elem = nil + @script_src_attr = nil @style_nonce = nil @style_src = nil + @style_src_elem = nil + @style_src_attr = nil @worker_src = nil @upgrade_insecure_requests = nil @disable_nonce_backwards_compatibility = nil diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 3d97c55a..b81f3b0e 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -78,6 +78,10 @@ def self.included(base) REQUIRE_SRI_FOR = :require_sri_for UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests WORKER_SRC = :worker_src + SCRIPT_SRC_ELEM = :script_src_elem + SCRIPT_SRC_ATTR = :script_src_attr + STYLE_SRC_ELEM = :style_src_elem + STYLE_SRC_ATTR = :style_src_attr DIRECTIVES_3_0 = [ DIRECTIVES_2_0, @@ -87,7 +91,11 @@ def self.included(base) PREFETCH_SRC, REQUIRE_SRI_FOR, WORKER_SRC, - UPGRADE_INSECURE_REQUESTS + UPGRADE_INSECURE_REQUESTS, + SCRIPT_SRC_ELEM, + SCRIPT_SRC_ATTR, + STYLE_SRC_ELEM, + STYLE_SRC_ATTR ].flatten.freeze ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort @@ -117,7 +125,11 @@ def self.included(base) PREFETCH_SRC => :source_list, SANDBOX => :sandbox_list, SCRIPT_SRC => :source_list, + SCRIPT_SRC_ELEM => :source_list, + SCRIPT_SRC_ATTR => :source_list, STYLE_SRC => :source_list, + STYLE_SRC_ELEM => :source_list, + STYLE_SRC_ATTR => :source_list, WORKER_SRC => :source_list, UPGRADE_INSECURE_REQUESTS => :boolean, }.freeze diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 4258da08..1a9ac4eb 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -49,6 +49,10 @@ module SecureHeaders style_src: %w('unsafe-inline'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ worker_src: %w(worker.com), + script_src_elem: %w(example.com), + script_src_attr: %w(example.com), + style_src_elem: %w(example.com), + style_src_attr: %w(example.com), report_uri: %w(https://example.com/uri-directive), } From b0190c59d1ba511504113c7780b014d7b6aa0fcc Mon Sep 17 00:00:00 2001 From: Guille Date: Fri, 5 Feb 2021 11:14:06 +0100 Subject: [PATCH 554/636] Change: update the README with the new directives --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e421a0ba..4cc5706f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,11 @@ SecureHeaders::Configuration.default do |config| sandbox: true, # true and [] will set a maximally restrictive setting plugin_types: %w(application/x-shockwave-flash), script_src: %w('self'), + script_src_elem: %w('self'), + script_src_attr: %w('self'), style_src: %w('unsafe-inline'), + style_src_elem: %w('unsafe-inline'), + style_src_attr: %w('unsafe-inline'), worker_src: %w('self'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) From 9bfb355c1714c44b0a29a675a708aea752d8e0dd Mon Sep 17 00:00:00 2001 From: Guille Date: Sun, 7 Feb 2021 16:41:58 +0100 Subject: [PATCH 555/636] Change: add specs to verify that the added directives are included in the CSP policy --- .../headers/content_security_policy_spec.rb | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index c1f24e6c..c080283d 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -160,6 +160,26 @@ module SecureHeaders csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456, disable_nonce_backwards_compatibility: true }) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'") end + + it "supports script-src-elem directive" do + csp = ContentSecurityPolicy.new({script_src: %w('self'), script_src_elem: %w('self')}) + expect(csp.value).to eq("script-src 'self'; script-src-elem 'self'") + end + + it "supports script-src-attr directive" do + csp = ContentSecurityPolicy.new({script_src: %w('self'), script_src_attr: %w('self')}) + expect(csp.value).to eq("script-src 'self'; script-src-attr 'self'") + end + + it "supports style-src-elem directive" do + csp = ContentSecurityPolicy.new({style_src: %w('self'), style_src_elem: %w('self')}) + expect(csp.value).to eq("style-src 'self'; style-src-elem 'self'") + end + + it "supports style-src-attr directive" do + csp = ContentSecurityPolicy.new({style_src: %w('self'), style_src_attr: %w('self')}) + expect(csp.value).to eq("style-src 'self'; style-src-attr 'self'") + end end end end From 5592e9ac79358ce5cfae2eb9b20228618aad3953 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 9 Feb 2021 08:57:13 -1000 Subject: [PATCH 556/636] bump to 6.3.2 --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64d85376..1ebb4ca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.3.2 + +Add support for style-src-attr, style-src-elem, script-src-attr, and script-src-elem directives (@ggalmazor) + ## 6.3.1 Fixes deprecation warnings when running under ruby 2.7 diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index c8090fc9..52b6f103 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.3.1" + VERSION = "6.3.2" end From 7aeb06f24bac07cc3642569cd090f09285f294a9 Mon Sep 17 00:00:00 2001 From: Marc Tremblay Date: Mon, 8 Mar 2021 11:26:23 +0100 Subject: [PATCH 557/636] Fix ClearSiteData example --- docs/per_action_configuration.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/per_action_configuration.md b/docs/per_action_configuration.md index c17a9e5f..dcf43787 100644 --- a/docs/per_action_configuration.md +++ b/docs/per_action_configuration.md @@ -120,9 +120,7 @@ You can clear the browser cache after the logout request by using the following. class ApplicationController < ActionController::Base # Configuration override to send the Clear-Site-Data header. SecureHeaders::Configuration.override(:clear_browser_cache) do |config| - config.clear_site_data = [ - SecureHeaders::ClearSiteData::ALL_TYPES - ] + config.clear_site_data = SecureHeaders::ClearSiteData::ALL_TYPES end From bba850a4fb5c29fa75c11405b6cd58710afc68d4 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Tue, 9 Mar 2021 09:57:50 -1000 Subject: [PATCH 558/636] Update ruby build scripts and bump test matrix versions --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcd9cd00..94c8b666 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,12 +7,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: [ '2.4', '2.5', '2.6', '2.7' ] + ruby: [ '2.5', '2.6', '2.7', '3.0' ] steps: - uses: actions/checkout@v2 - name: Set up Ruby ${{ matrix.ruby }} - uses: actions/setup-ruby@v1 + uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Build and test with Rake From c32e49f276ab18d342eb0c2db4e4e1a47f19b728 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Tue, 9 Mar 2021 10:15:36 -1000 Subject: [PATCH 559/636] Delete sync.yml --- .github/workflows/sync.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/workflows/sync.yml diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml deleted file mode 100644 index 319e8470..00000000 --- a/.github/workflows/sync.yml +++ /dev/null @@ -1,20 +0,0 @@ -# This workflow ensures the "master" branch is always up-to-date with the -# "main" branch (our default one) -name: sync_main_branch -on: - push: - branches: [ main ] -jobs: - catch_up: - runs-on: ubuntu-latest - steps: - - name: Check out the repository - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Merge development into master, then push it - run: | - git pull - git checkout master - git merge main - git push From 159bbdb0b61ca7c41cbaa49fd3b76c1646117273 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Wed, 28 Apr 2021 07:17:16 -1000 Subject: [PATCH 560/636] Update per_action_configuration.md --- docs/per_action_configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/per_action_configuration.md b/docs/per_action_configuration.md index dcf43787..63716dba 100644 --- a/docs/per_action_configuration.md +++ b/docs/per_action_configuration.md @@ -54,7 +54,7 @@ Code | Result #### Nonce -You can use a view helper to automatically add nonces to script tags: +You can use a view helper to automatically add nonces to script tags. Currently, using a nonce helper or calling `content_security_policy_nonce` will populate all configured CSP headers, including report-only and enforced policies. ```erb <%= nonced_javascript_tag do %> From 8e28012493298c622cf811b6ea1fa8a198062f35 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Thu, 6 May 2021 10:30:21 -1000 Subject: [PATCH 561/636] Add https://github.com/TypeError/secure to list of similar libraries cc @cak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cc5706f..5c53fba8 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ If you've made a contribution and see your name missing from the list, make a PR * Node.js (express) [helmet](https://github.com/helmetjs/helmet) and [hood](https://github.com/seanmonstar/hood) * Node.js (hapi) [blankie](https://github.com/nlf/blankie) * ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) -* Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) +* Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security), [secure](https://github.com/TypeError/secure) * Go - [secureheader](https://github.com/kr/secureheader) * Elixir [secure_headers](https://github.com/anotherhale/secure_headers) * Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security) From a0072e096e887d185c9275b803995a39ea4b4c18 Mon Sep 17 00:00:00 2001 From: Ryan Ahearn Date: Fri, 27 Aug 2021 16:25:22 -0400 Subject: [PATCH 562/636] Fix hash generation for indented helper methods The helper outputs the closing or unindented if it is on its own line. Needed to remove any extra whitespace the regex captures on that last line. --- lib/tasks/tasks.rake | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake index 374fea5e..cb078246 100644 --- a/lib/tasks/tasks.rake +++ b/lib/tasks/tasks.rake @@ -20,10 +20,11 @@ namespace :secure_headers do (is_erb?(filename) && inline_script =~ /<%.*%>/) end - def find_inline_content(filename, regex, hashes) + def find_inline_content(filename, regex, hashes, strip_trailing_whitespace) file = File.read(filename) file.scan(regex) do # TODO don't use gsub inline_script = Regexp.last_match.captures.last + inline_script.gsub!(/(\r?\n)[\t ]+\z/, '\1') if strip_trailing_whitespace if dynamic_content?(filename, inline_script) puts "Looks like there's some dynamic content inside of a tag :-/" puts "That pretty much means the hash value will never match." @@ -38,9 +39,8 @@ namespace :secure_headers do def generate_inline_script_hashes(filename) hashes = [] - [INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex| - find_inline_content(filename, regex, hashes) - end + find_inline_content(filename, INLINE_SCRIPT_REGEX, hashes, false) + find_inline_content(filename, INLINE_HASH_SCRIPT_HELPER_REGEX, hashes, true) hashes end @@ -48,9 +48,8 @@ namespace :secure_headers do def generate_inline_style_hashes(filename) hashes = [] - [INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex| - find_inline_content(filename, regex, hashes) - end + find_inline_content(filename, INLINE_STYLE_REGEX, hashes, false) + find_inline_content(filename, INLINE_HASH_STYLE_HELPER_REGEX, hashes, true) hashes end From e4a198c0f40aaae697a51bd4c520133f8d3980de Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Tue, 7 Sep 2021 12:12:56 -1000 Subject: [PATCH 563/636] bump to 6.3.3 --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ebb4ca6..b0bf80eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.3.3 + +Fix hash generation for indented helper methods (@rahearn) + ## 6.3.2 Add support for style-src-attr, style-src-elem, script-src-attr, and script-src-elem directives (@ggalmazor) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 52b6f103..57500044 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.3.2" + VERSION = "6.3.3" end From 7f89df2dafb22f1833702eef4a01d4794b2066f0 Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Mon, 15 Nov 2021 06:06:24 +0000 Subject: [PATCH 564/636] don't limit rubocop version --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index a6b3e54d..78cca83b 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ group :test do gem "pry-nav" gem "rack" gem "rspec" - gem "rubocop", "< 0.68" + gem "rubocop" gem "rubocop-github" gem "rubocop-performance" gem "term-ansicolor" From 1bd1e4c0106d0d2ec5d3ed2af8f22288b06c28b5 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 1 Apr 2022 11:58:36 +0100 Subject: [PATCH 565/636] do not dedupe alternate schema source expresions --- lib/secure_headers/headers/content_security_policy.rb | 2 +- .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index cb75050c..2df477dc 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -157,7 +157,7 @@ def dedup_source_list(sources) if wild_sources.any? sources.reject do |source| !wild_sources.include?(source) && - wild_sources.any? { |pattern| File.fnmatch(pattern, source) } + wild_sources.any? { |pattern| URI(pattern).scheme == URI(source).scheme && File.fnmatch(pattern, source) } end else sources diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index c080283d..35a8c3ef 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -106,6 +106,11 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org") end + it "does not deduplicate non-matching schema source expressions" do + csp = ContentSecurityPolicy.new(default_src: %w(*.example.org wss://*.example.org)) + expect(csp.value).to eq("default-src *.example.org wss://*.example.org") + end + it "creates maximally strict sandbox policy when passed no sandbox token values" do csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: []) expect(csp.value).to eq("default-src example.org; sandbox") From 7e8d967684e8fc7c49aaec7c66fbecd34f711f71 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 1 Apr 2022 14:59:01 +0100 Subject: [PATCH 566/636] precompute schemes map before any loop --- lib/secure_headers/headers/content_security_policy.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 2df477dc..9381cd9a 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -155,9 +155,10 @@ def dedup_source_list(sources) wild_sources = sources.select { |source| source =~ STAR_REGEXP } if wild_sources.any? + schemes = sources.map { |source| [source, URI(source).scheme] }.to_h sources.reject do |source| !wild_sources.include?(source) && - wild_sources.any? { |pattern| URI(pattern).scheme == URI(source).scheme && File.fnmatch(pattern, source) } + wild_sources.any? { |pattern| schemes[pattern] == schemes[source] && File.fnmatch(pattern, source) } end else sources From 78cb949f8faf0ab41d49cbb1df9e8e412334523a Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 1 Apr 2022 14:59:23 +0100 Subject: [PATCH 567/636] fix test to properly fail without scheme pattern match --- .../secure_headers/headers/content_security_policy_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 35a8c3ef..dbd878a9 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -107,8 +107,8 @@ module SecureHeaders end it "does not deduplicate non-matching schema source expressions" do - csp = ContentSecurityPolicy.new(default_src: %w(*.example.org wss://*.example.org)) - expect(csp.value).to eq("default-src *.example.org wss://*.example.org") + csp = ContentSecurityPolicy.new(default_src: %w(*.example.org wss://example.example.org)) + expect(csp.value).to eq("default-src *.example.org wss://example.example.org") end it "creates maximally strict sandbox policy when passed no sandbox token values" do From daa06621115c7ab69d273b89b735412f24fd7488 Mon Sep 17 00:00:00 2001 From: akashhansda <99724223+akashhansda@users.noreply.github.com> Date: Thu, 7 Apr 2022 08:37:50 -0700 Subject: [PATCH 568/636] Update README.md Use https --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c53fba8..1075b083 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **main branch represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. The gem will automatically apply several headers that are related to security. This includes: -- Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) +- Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](https://www.w3.org/TR/CSP2/) - https://csp.withgoogle.com - https://csp.withgoogle.com/docs/strict-csp.html - https://csp-evaluator.withgoogle.com From 034c8b60b38a5ed169f219ee00a90d09e69992a6 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Thu, 5 May 2022 14:43:26 -0700 Subject: [PATCH 569/636] Update one more `README.md` reference to HTTPS. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1075b083..b842cac4 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ SecureHeaders::Configuration.default do |config| # directive values: these values will directly translate into source directives default_src: %w('none'), base_uri: %w('self'), - block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ + block_all_mixed_content: true, # see https://www.w3.org/TR/mixed-content/ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. connect_src: %w(wss:), font_src: %w('self' data:), From fac659e54dd7905f7d068eea3dd5d1ac403230af Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Wed, 22 Jun 2022 11:48:38 -0700 Subject: [PATCH 570/636] Update changelog and version for v6.3.4. --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0bf80eb..69b42639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.3.4 + +- CSP: Do not deduplicate alternate schema source expressions (@keithamus): https://github.com/github/secure_headers/pull/478 + ## 6.3.3 Fix hash generation for indented helper methods (@rahearn) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 57500044..22169825 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.3.3" + VERSION = "6.3.4" end From e6db1f848d59c716ba9c26a23e5f8d58f19fa313 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Mon, 27 Jun 2022 15:58:47 -0700 Subject: [PATCH 571/636] Add a workflow to automatically relase version tags. --- .github/workflows/github-release.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/github-release.yml diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 00000000..ef6018a2 --- /dev/null +++ b/.github/workflows/github-release.yml @@ -0,0 +1,28 @@ +name: GitHub Release + +on: + push: + tags: + - v* + +jobs: + Publish: + permissions: + contents: write + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Calculate release name + run: | + GITHUB_REF=${{ github.ref }} + RELEASE_NAME=${GITHUB_REF#"refs/tags/"} + echo "RELEASE_NAME=${RELEASE_NAME}" >> $GITHUB_ENV + - name: Publish release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ env.RELEASE_NAME }} + draft: false + prerelease: false From 57d8995e6dfc7c545393885cac56da89a9045623 Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Mon, 27 Jun 2022 16:23:27 -0700 Subject: [PATCH 572/636] Add Ruby 3.1 to the CI configuration Also made several formatting changes to get Rubocop to be green. --- .github/workflows/build.yml | 2 +- lib/secure_headers/configuration.rb | 11 +++---- .../headers/content_security_policy.rb | 30 ++++++++++--------- .../headers/policy_management.rb | 11 +++---- lib/secure_headers/view_helper.rb | 13 ++++---- spec/lib/secure_headers/view_helpers_spec.rb | 9 +++--- 6 files changed, 41 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94c8b666..0c7faef8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: [ '2.5', '2.6', '2.7', '3.0' ] + ruby: [ '2.5', '2.6', '2.7', '3.0', '3.1' ] steps: - uses: actions/checkout@v2 diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 538365a3..996ca74f 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -84,11 +84,12 @@ def named_append_or_override_exists?(name) def deep_copy(config) return unless config config.each_with_object({}) do |(key, value), hash| - hash[key] = if value.is_a?(Array) - value.dup - else - value - end + hash[key] = + if value.is_a?(Array) + value.dup + else + value + end end end diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 9381cd9a..38ac6cc0 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -7,17 +7,18 @@ class ContentSecurityPolicy include PolicyManagement def initialize(config = nil) - @config = if config.is_a?(Hash) - if config[:report_only] - ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG) + @config = + if config.is_a?(Hash) + if config[:report_only] + ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG) + else + ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG) + end + elsif config.nil? + ContentSecurityPolicyConfig.new(DEFAULT_CONFIG) else - ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG) + config end - elsif config.nil? - ContentSecurityPolicyConfig.new(DEFAULT_CONFIG) - else - config - end @preserve_schemes = @config.preserve_schemes @script_nonce = @config.script_nonce @@ -34,11 +35,12 @@ def name ## # Return the value of the CSP header def value - @value ||= if @config - build_value - else - DEFAULT_VALUE - end + @value ||= + if @config + build_value + else + DEFAULT_VALUE + end end private diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index b81f3b0e..d3ce5702 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -278,11 +278,12 @@ def list_directive?(directive) def populate_fetch_source_with_default!(original, additions) # in case we would be appending to an empty directive, fill it with the default-src value additions.each_key do |directive| - directive = if directive.to_s.end_with?("_nonce") - directive.to_s.gsub(/_nonce/, "_src").to_sym - else - directive - end + directive = + if directive.to_s.end_with?("_nonce") + directive.to_s.gsub(/_nonce/, "_src").to_sym + else + directive + end # Don't set a default if directive has an existing value next if original[directive] if FETCH_SOURCES.include?(directive) diff --git a/lib/secure_headers/view_helper.rb b/lib/secure_headers/view_helper.rb index 91eef4d5..7ed57311 100644 --- a/lib/secure_headers/view_helper.rb +++ b/lib/secure_headers/view_helper.rb @@ -147,12 +147,13 @@ def unexpected_hash_error_message(file_path, content, hash_value) def nonced_tag(type, content_or_options, block) options = {} - content = if block - options = content_or_options - capture(&block) - else - content_or_options.html_safe # :'( - end + content = + if block + options = content_or_options + capture(&block) + else + content_or_options.html_safe # :'( + end content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type)) end diff --git a/spec/lib/secure_headers/view_helpers_spec.rb b/spec/lib/secure_headers/view_helpers_spec.rb index c71534a1..2a7f56ed 100644 --- a/spec/lib/secure_headers/view_helpers_spec.rb +++ b/spec/lib/secure_headers/view_helpers_spec.rb @@ -6,7 +6,7 @@ class Message < ERB include SecureHeaders::ViewHelpers def self.template -<<-TEMPLATE + <<-TEMPLATE <% hashed_javascript_tag(raise_error_on_unrecognized_hash = true) do %> console.log(1) <% end %> @@ -62,9 +62,10 @@ def capture(*args) end def content_tag(type, content = nil, options = nil, &block) - content = if block_given? - capture(block) - end + content = + if block_given? + capture(block) + end if options.is_a?(Hash) options = options.map { |k, v| " #{k}=#{v}" } From 65c917240e4b4698d9514c22b7ea0eadacd9664e Mon Sep 17 00:00:00 2001 From: Jack McCracken Date: Wed, 4 May 2022 15:37:38 -0400 Subject: [PATCH 573/636] Add trusted types directive --- .../headers/content_security_policy_config.rb | 2 ++ .../headers/policy_management.rb | 30 ++++++++++++++++++- .../headers/content_security_policy_spec.rb | 14 +++++++++ .../headers/policy_management_spec.rb | 2 ++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 3c477161..5d3c7550 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -35,6 +35,7 @@ def initialize(hash) @report_only = nil @report_uri = nil @require_sri_for = nil + @require_trusted_types_for = nil @sandbox = nil @script_nonce = nil @script_src = nil @@ -44,6 +45,7 @@ def initialize(hash) @style_src = nil @style_src_elem = nil @style_src_attr = nil + @trusted_types = nil @worker_src = nil @upgrade_insecure_requests = nil @disable_nonce_backwards_compatibility = nil diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index d3ce5702..4c69111f 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "set" +require 'byebug' module SecureHeaders module PolicyManagement @@ -98,7 +99,19 @@ def self.included(base) STYLE_SRC_ATTR ].flatten.freeze - ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort + # Experimental directives - these vary greatly in support + # See MDN for details. + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types + TRUSTED_TYPES = :trusted_types + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for + REQUIRE_TRUSTED_TYPES_FOR = :require_trusted_types_for + + DIRECTIVES_EXPERIMENTAL = [ + TRUSTED_TYPES, + REQUIRE_TRUSTED_TYPES_FOR, + ].flatten.freeze + + ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_EXPERIMENTAL).uniq.sort # Think of default-src and report-uri as the beginning and end respectively, # everything else is in between. @@ -121,6 +134,7 @@ def self.included(base) OBJECT_SRC => :source_list, PLUGIN_TYPES => :media_type_list, REQUIRE_SRI_FOR => :require_sri_for_list, + REQUIRE_TRUSTED_TYPES_FOR => :require_trusted_types_for_list, REPORT_URI => :source_list, PREFETCH_SRC => :source_list, SANDBOX => :sandbox_list, @@ -130,6 +144,7 @@ def self.included(base) STYLE_SRC => :source_list, STYLE_SRC_ELEM => :source_list, STYLE_SRC_ATTR => :source_list, + TRUSTED_TYPES => :source_list, WORKER_SRC => :source_list, UPGRADE_INSECURE_REQUESTS => :boolean, }.freeze @@ -175,6 +190,7 @@ def self.included(base) ].freeze REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style)) + REQUIRE_TRUSTED_TYPES_FOR_VALUES = Set.new(%w(script)) module ClassMethods # Public: generate a header name, value array that is user-agent-aware. @@ -325,6 +341,8 @@ def validate_directive!(directive, value) validate_media_type_expression!(directive, value) when :require_sri_for_list validate_require_sri_source_expression!(directive, value) + when :require_trusted_types_for_list + validate_require_trusted_types_for_source_expression!(directive, value) else raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") end @@ -369,6 +387,16 @@ def validate_require_sri_source_expression!(directive, require_sri_for_expressio end end + # Private: validates that a require trusted types for expression: + # 1. is an array of strings + # 2. is a subset of ["script"] + def validate_require_trusted_types_for_source_expression!(directive, require_trusted_types_for_expression) + ensure_array_of_strings!(directive, require_trusted_types_for_expression) + unless require_trusted_types_for_expression.to_set.subset?(REQUIRE_TRUSTED_TYPES_FOR_VALUES) + raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_TRUSTED_TYPES_FOR_VALUES.to_a} but was #{require_trusted_types_for_expression})) + end + end + # Private: validates that a source expression: # 1. is an array of strings # 2. does not contain any deprecated, now invalid values (inline, eval, self, none) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index dbd878a9..ce4256fe 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -146,6 +146,15 @@ module SecureHeaders expect(csp.value).to eq("default-src 'self'; require-sri-for script style") end + it "supports require-trusted-types-for directive" do + csp = ContentSecurityPolicy.new({require_trusted_types_for: %(script)}) + expect(csp.value).to eq("require-trusted-types-for script") + end + + it "does not support style for require-trusted-types-for directive" do + expect { ContentSecurityPolicy.new({require_trusted_types_for: %(script style)}) }.to raise_error(ContentSecurityPolicyConfigError) + end + it "includes prefetch-src" do csp = ContentSecurityPolicy.new(default_src: %w('self'), prefetch_src: %w(foo.com)) expect(csp.value).to eq("default-src 'self'; prefetch-src foo.com") @@ -185,6 +194,11 @@ module SecureHeaders csp = ContentSecurityPolicy.new({style_src: %w('self'), style_src_attr: %w('self')}) expect(csp.value).to eq("style-src 'self'; style-src-attr 'self'") end + + it "supports trusted-types directive" do + csp = ContentSecurityPolicy.new({trusted_types: %w(blahblahpolicy)}) + expect(csp.value).to eq("trusted-types blahblahpolicy") + end end end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 1a9ac4eb..150e0b35 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -45,6 +45,7 @@ module SecureHeaders plugin_types: %w(application/x-shockwave-flash), prefetch_src: %w(fetch.com), require_sri_for: %w(script style), + require_trusted_types_for: %w(script), script_src: %w('self'), style_src: %w('unsafe-inline'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ @@ -53,6 +54,7 @@ module SecureHeaders script_src_attr: %w(example.com), style_src_elem: %w(example.com), style_src_attr: %w(example.com), + trusted_types: %w(abcpolicy), report_uri: %w(https://example.com/uri-directive), } From e78ae24e2527917079b8bbe2e5972ab37b0ddc04 Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Thu, 12 May 2022 18:34:40 +0000 Subject: [PATCH 574/636] just enough to get the trusted types directive into the csp, I think --- lib/secure_headers/headers/content_security_policy.rb | 10 ++++++++++ lib/secure_headers/headers/policy_management.rb | 7 ++++++- .../headers/content_security_policy_spec.rb | 4 ++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 38ac6cc0..6e80272f 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -61,10 +61,20 @@ def build_value build_sandbox_list_directive(directive_name) when :media_type_list build_media_type_list_directive(directive_name) + when :require_trusted_types_for_list + build_trusted_type_list_directive(directive_name) end end.compact.join("; ") end + def build_trusted_type_list_directive(directive) + source_list = @config.directive_value(directive) + if source_list && !source_list.empty? + escaped_source_list = source_list.gsub(/[\n;]/, " ") + [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip + end + end + def build_sandbox_list_directive(directive) return unless sandbox_list = @config.directive_value(directive) max_strict_policy = case sandbox_list diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 4c69111f..4c686e74 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -286,7 +286,8 @@ def list_directive?(directive) source_list?(directive) || sandbox_list?(directive) || media_type_list?(directive) || - require_sri_for_list?(directive) + require_sri_for_list?(directive) || + require_trusted_types_for_list?(directive) end # For each directive in additions that does not exist in the original config, @@ -324,6 +325,10 @@ def require_sri_for_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list end + def require_trusted_types_for_list?(directive) + DIRECTIVE_VALUE_TYPES[directive] == :require_trusted_types_for_list + end + # Private: Validates that the configuration has a valid type, or that it is a valid # source expression. def validate_directive!(directive, value) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index ce4256fe..f6471f43 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -147,8 +147,8 @@ module SecureHeaders end it "supports require-trusted-types-for directive" do - csp = ContentSecurityPolicy.new({require_trusted_types_for: %(script)}) - expect(csp.value).to eq("require-trusted-types-for script") + csp = ContentSecurityPolicy.new(default_src: %w('self'), require_trusted_types_for: %(script)) + expect(csp.value).to eq("default-src 'self'; require-trusted-types-for script") end it "does not support style for require-trusted-types-for directive" do From 21b2f46cabca53d734659bb2b5d2c4f598d83cef Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Thu, 26 May 2022 13:41:06 +0000 Subject: [PATCH 575/636] fix test syntax --- lib/secure_headers/headers/content_security_policy.rb | 4 +--- lib/secure_headers/headers/policy_management.rb | 2 +- .../secure_headers/headers/content_security_policy_spec.rb | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 6e80272f..63373dd3 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -53,7 +53,7 @@ def value def build_value directives.map do |directive_name| case DIRECTIVE_VALUE_TYPES[directive_name] - when :source_list, :require_sri_for_list # require_sri is a simple set of strings that don't need to deal with symbol casing + when :source_list, :require_sri_for_list, :require_trusted_types_for_list # require_sri is a simple set of strings that don't need to deal with symbol casing build_source_list_directive(directive_name) when :boolean symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) @@ -61,8 +61,6 @@ def build_value build_sandbox_list_directive(directive_name) when :media_type_list build_media_type_list_directive(directive_name) - when :require_trusted_types_for_list - build_trusted_type_list_directive(directive_name) end end.compact.join("; ") end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 4c686e74..0a92716d 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -398,7 +398,7 @@ def validate_require_sri_source_expression!(directive, require_sri_for_expressio def validate_require_trusted_types_for_source_expression!(directive, require_trusted_types_for_expression) ensure_array_of_strings!(directive, require_trusted_types_for_expression) unless require_trusted_types_for_expression.to_set.subset?(REQUIRE_TRUSTED_TYPES_FOR_VALUES) - raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_TRUSTED_TYPES_FOR_VALUES.to_a} but was #{require_trusted_types_for_expression})) + raise ContentSecurityPolicyConfigError.new(%(require-trusted-types-for for must be a subset of #{REQUIRE_TRUSTED_TYPES_FOR_VALUES.to_a} but was #{require_trusted_types_for_expression})) end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index f6471f43..a190ab97 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -147,7 +147,7 @@ module SecureHeaders end it "supports require-trusted-types-for directive" do - csp = ContentSecurityPolicy.new(default_src: %w('self'), require_trusted_types_for: %(script)) + csp = ContentSecurityPolicy.new(default_src: %w('self'), require_trusted_types_for: %w(script)) expect(csp.value).to eq("default-src 'self'; require-trusted-types-for script") end From 79a2b5dce28da0221cdbb244f52e9c7f556d7b8b Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Tue, 7 Jun 2022 17:31:17 +0000 Subject: [PATCH 576/636] remove unused function --- lib/secure_headers/headers/content_security_policy.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 63373dd3..3c3c0c7e 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -65,14 +65,6 @@ def build_value end.compact.join("; ") end - def build_trusted_type_list_directive(directive) - source_list = @config.directive_value(directive) - if source_list && !source_list.empty? - escaped_source_list = source_list.gsub(/[\n;]/, " ") - [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip - end - end - def build_sandbox_list_directive(directive) return unless sandbox_list = @config.directive_value(directive) max_strict_policy = case sandbox_list From f2cc4c9f813f94d205826aa8ac191e0476d0f045 Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Tue, 7 Jun 2022 17:48:00 +0000 Subject: [PATCH 577/636] move trusted types validate config to appropriate test file --- .../secure_headers/headers/content_security_policy_spec.rb | 4 ---- spec/lib/secure_headers/headers/policy_management_spec.rb | 6 ++++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index a190ab97..3e5c609b 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -151,10 +151,6 @@ module SecureHeaders expect(csp.value).to eq("default-src 'self'; require-trusted-types-for script") end - it "does not support style for require-trusted-types-for directive" do - expect { ContentSecurityPolicy.new({require_trusted_types_for: %(script style)}) }.to raise_error(ContentSecurityPolicyConfigError) - end - it "includes prefetch-src" do csp = ContentSecurityPolicy.new(default_src: %w('self'), prefetch_src: %w(foo.com)) expect(csp.value).to eq("default-src 'self'; prefetch-src foo.com") diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 150e0b35..d9c85279 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -122,6 +122,12 @@ module SecureHeaders end.to raise_error(ContentSecurityPolicyConfigError) end + it "rejects style for trusted types" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(style_src: %w('self'), require_trusted_types_for: %w(script style), trusted_types: %w(abcpolicy)))) + end + end + # this is mostly to ensure people don't use the antiquated shorthands common in other configs it "performs light validation on source lists" do expect do From f10a4a4dd0429d02342603f30944bc896000f6f6 Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Wed, 8 Jun 2022 15:08:05 +0000 Subject: [PATCH 578/636] remove debugging in file --- lib/secure_headers/headers/policy_management.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 0a92716d..a4d9635e 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "set" -require 'byebug' module SecureHeaders module PolicyManagement From ba3cf0288dfe4f25fbed639ba28b8701d7dc1a66 Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Wed, 8 Jun 2022 15:10:18 +0000 Subject: [PATCH 579/636] make test match others and add tesfor allow-duplicates --- .../secure_headers/headers/content_security_policy_spec.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 3e5c609b..c57edca1 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -146,7 +146,7 @@ module SecureHeaders expect(csp.value).to eq("default-src 'self'; require-sri-for script style") end - it "supports require-trusted-types-for directive" do + it "allows style as a require-trusted-types-src" do csp = ContentSecurityPolicy.new(default_src: %w('self'), require_trusted_types_for: %w(script)) expect(csp.value).to eq("default-src 'self'; require-trusted-types-for script") end @@ -195,6 +195,11 @@ module SecureHeaders csp = ContentSecurityPolicy.new({trusted_types: %w(blahblahpolicy)}) expect(csp.value).to eq("trusted-types blahblahpolicy") end + + it "allows duplicate policy names in trusted-types directive" do + csp = ContentSecurityPolicy.new({trusted_types: %w(blahblahpolicy 'allow-duplicates')}) + expect(csp.value).to eq("trusted-types blahblahpolicy 'allow-duplicates'") + end end end end From 98674532f92a961a82dc99553af24ec6b2a7a63b Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Mon, 27 Jun 2022 13:06:24 -0400 Subject: [PATCH 580/636] Apply suggestions from code review Co-authored-by: Lucas Garron --- lib/secure_headers/headers/content_security_policy.rb | 4 +++- .../secure_headers/headers/content_security_policy_spec.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 3c3c0c7e..7fc7c186 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -53,7 +53,9 @@ def value def build_value directives.map do |directive_name| case DIRECTIVE_VALUE_TYPES[directive_name] - when :source_list, :require_sri_for_list, :require_trusted_types_for_list # require_sri is a simple set of strings that don't need to deal with symbol casing + when :source_list, + :require_sri_for_list, # require_sri is a simple set of strings that don't need to deal with symbol casing + :require_trusted_types_for_list build_source_list_directive(directive_name) when :boolean symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index c57edca1..73222a29 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -146,7 +146,7 @@ module SecureHeaders expect(csp.value).to eq("default-src 'self'; require-sri-for script style") end - it "allows style as a require-trusted-types-src" do + it "allows style as a require-trusted-types-for source" do csp = ContentSecurityPolicy.new(default_src: %w('self'), require_trusted_types_for: %w(script)) expect(csp.value).to eq("default-src 'self'; require-trusted-types-for script") end From 071c580fe60f684dba31c9fc93a1dbe13165c898 Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Mon, 27 Jun 2022 20:48:27 +0000 Subject: [PATCH 581/636] fix whitespace --- lib/secure_headers/headers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 7fc7c186..2c9ef501 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -55,7 +55,7 @@ def build_value case DIRECTIVE_VALUE_TYPES[directive_name] when :source_list, :require_sri_for_list, # require_sri is a simple set of strings that don't need to deal with symbol casing - :require_trusted_types_for_list + :require_trusted_types_for_list build_source_list_directive(directive_name) when :boolean symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) From f40910c4fe23c6d9398b762ceccafe0e2387bb66 Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:38:40 +0000 Subject: [PATCH 582/636] add test for 'none' trusted type --- .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 73222a29..53f8cfa2 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -196,6 +196,11 @@ module SecureHeaders expect(csp.value).to eq("trusted-types blahblahpolicy") end + it "supports trusted-types directive with 'none'" do + csp = ContentSecurityPolicy.new({trusted_types: %w(none)}) + expect(csp.value).to eq("trusted-types none") + end + it "allows duplicate policy names in trusted-types directive" do csp = ContentSecurityPolicy.new({trusted_types: %w(blahblahpolicy 'allow-duplicates')}) expect(csp.value).to eq("trusted-types blahblahpolicy 'allow-duplicates'") From 2bf5521c6e50984dec735cc90b02e0e2cc861a65 Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Mon, 27 Jun 2022 16:02:00 -0700 Subject: [PATCH 583/636] Add Dependabot for GitHub Actions --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 6a3b75d1225e8c4d919f6afaa79c0315f2a52c26 Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Tue, 26 Jul 2022 16:36:18 +0200 Subject: [PATCH 584/636] Use SPDX license code and swap summary & description Using an SPDX code makes automatic processing easier. The summary should be shorter than the description. --- secure_headers.gemspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index c5f7292c..f332ce72 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -9,12 +9,12 @@ Gem::Specification.new do |gem| gem.version = SecureHeaders::VERSION gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] - gem.description = "Manages application of security headers with many safe defaults." - gem.summary = 'Add easily configured security headers to responses + gem.summary = "Manages application of security headers with many safe defaults." + gem.description = 'Add easily configured security headers to responses including content-security-policy, x-frame-options, strict-transport-security, etc.' gem.homepage = "https://github.com/twitter/secureheaders" - gem.license = "Apache Public License 2.0" + gem.license = "Apache-2.0" gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) From 084f5174826146cb4b8c88772ea3ce1fd7f4166f Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Tue, 2 Aug 2022 15:41:52 -0400 Subject: [PATCH 585/636] update version and changelog (#494) * update version and changelog * add PR link to changelog --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b42639..4fd0ce3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.4.0 + +- CSP: Add support for trusted-types, require-trusted-types-for directive (@JackMc): https://github.com/github/secure_headers/pull/486 + ## 6.3.4 - CSP: Do not deduplicate alternate schema source expressions (@keithamus): https://github.com/github/secure_headers/pull/478 diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 22169825..2614e5fe 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.3.4" + VERSION = "6.4.0" end From 0e8f4999dd3c523399426a4f693770122f30fba1 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Tue, 9 Aug 2022 19:55:47 -0700 Subject: [PATCH 586/636] Trusted types: Use single-quoted `'script'`. Contrary to e.g. `require-sri-for` (which our previous implementation was for), it turns out the `require-trusted-types-for` directive uses: - Single-quoted `'script'` and `'none'` sources (in addition to `'allow-duplicates'`). - Unquoted policies in addition to those. See: - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-sri-for - https://w3c.github.io/webappsec-trusted-types/dist/spec/#require-trusted-types-for-csp-directive Right now we are flexible about both quoted and unquoted sources, but this PR starts by using the values used for the directive per spec / browser implementations. --- lib/secure_headers/headers/policy_management.rb | 4 ++-- .../secure_headers/headers/content_security_policy_spec.rb | 4 ++-- spec/lib/secure_headers/headers/policy_management_spec.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index a4d9635e..1d464f4c 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -189,7 +189,7 @@ def self.included(base) ].freeze REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style)) - REQUIRE_TRUSTED_TYPES_FOR_VALUES = Set.new(%w(script)) + REQUIRE_TRUSTED_TYPES_FOR_VALUES = Set.new(%w('script')) module ClassMethods # Public: generate a header name, value array that is user-agent-aware. @@ -393,7 +393,7 @@ def validate_require_sri_source_expression!(directive, require_sri_for_expressio # Private: validates that a require trusted types for expression: # 1. is an array of strings - # 2. is a subset of ["script"] + # 2. is a subset of ["'script'"] def validate_require_trusted_types_for_source_expression!(directive, require_trusted_types_for_expression) ensure_array_of_strings!(directive, require_trusted_types_for_expression) unless require_trusted_types_for_expression.to_set.subset?(REQUIRE_TRUSTED_TYPES_FOR_VALUES) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 53f8cfa2..56397600 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -197,8 +197,8 @@ module SecureHeaders end it "supports trusted-types directive with 'none'" do - csp = ContentSecurityPolicy.new({trusted_types: %w(none)}) - expect(csp.value).to eq("trusted-types none") + csp = ContentSecurityPolicy.new({trusted_types: %w('none')}) + expect(csp.value).to eq("trusted-types 'none'") end it "allows duplicate policy names in trusted-types directive" do diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index d9c85279..0f7cf8a8 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -45,7 +45,7 @@ module SecureHeaders plugin_types: %w(application/x-shockwave-flash), prefetch_src: %w(fetch.com), require_sri_for: %w(script style), - require_trusted_types_for: %w(script), + require_trusted_types_for: %w('script'), script_src: %w('self'), style_src: %w('unsafe-inline'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ From 02d65f10d76484f51dac1f2f8090f942cd8d0a21 Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Wed, 10 Aug 2022 23:39:06 +0200 Subject: [PATCH 587/636] Set license code in metadata to MIT In 5638cb03344a8b5f02024735143d2261cff08a94 the gem was relicensed to MIT, but it was incomplete. 86c762aea480d0a776246652586f93e026f6799f at least fixed the README, but the gemspec itself was still forgotten. Fixes: 5638cb03344a8b5f02024735143d2261cff08a94 --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index f332ce72..4880c3ff 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |gem| including content-security-policy, x-frame-options, strict-transport-security, etc.' gem.homepage = "https://github.com/twitter/secureheaders" - gem.license = "Apache-2.0" + gem.license = "MIT" gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) From 388b8faf74b08b1e78d38c348d9a4005c91fa68b Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Wed, 19 Oct 2022 13:00:20 -0700 Subject: [PATCH 588/636] Run CI on push. CI doesn't have access to any secrets (like a RubyGems auth token) or production data, so it's fairly safe to run on every push. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c7faef8..a28c4ad8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ name: Build + Test -on: [pull_request] +on: [pull_request, push] jobs: build: From 53f2eac6751766f97d523722ea7d131d88f6419c Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Wed, 19 Oct 2022 11:40:49 -0700 Subject: [PATCH 589/636] Remove Ruby 2.5 from CI. Ruby 2.5 has trouble connecting to `coveralls.io`. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a28c4ad8..3588214c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: [ '2.5', '2.6', '2.7', '3.0', '3.1' ] + ruby: [ '2.6', '2.7', '3.0', '3.1' ] steps: - uses: actions/checkout@v2 From af92ce6f09ed96fbad8836eae8739b0191b25dd5 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Fri, 14 Oct 2022 15:16:25 -0700 Subject: [PATCH 590/636] Add a regression test for a current bug. --- .../secure_headers/headers/content_security_policy_spec.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 56397600..b3e64c92 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -101,6 +101,11 @@ module SecureHeaders expect(csp.value).to eq("default-src example.org; block-all-mixed-content") end + it "handles wildcard subdomain with wildcard port" do + csp = ContentSecurityPolicy.new(default_src: %w(https://*.example.org:*)) + expect(csp.value).to eq("default-src *.example.org:*") + end + it "deduplicates any source expressions" do csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org)) expect(csp.value).to eq("default-src example.org") From cdb40e5b851d56b479892b521cd9eb0b1b1a6a83 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Wed, 19 Oct 2022 05:20:48 -0700 Subject: [PATCH 591/636] Remove `dedup_source_list` and replace with a "best-effort" `.uniq` call. --- .../headers/content_security_policy.rb | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 2c9ef501..566004b8 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -133,7 +133,7 @@ def minify_source_list(directive, source_list) unless directive == REPORT_URI || @preserve_schemes source_list = strip_source_schemes(source_list) end - dedup_source_list(source_list) + source_list.uniq end end @@ -151,24 +151,6 @@ def reject_all_values_if_none(source_list) end end - # Removes duplicates and sources that already match an existing wild card. - # - # e.g. *.github.com asdf.github.com becomes *.github.com - def dedup_source_list(sources) - sources = sources.uniq - wild_sources = sources.select { |source| source =~ STAR_REGEXP } - - if wild_sources.any? - schemes = sources.map { |source| [source, URI(source).scheme] }.to_h - sources.reject do |source| - !wild_sources.include?(source) && - wild_sources.any? { |pattern| schemes[pattern] == schemes[source] && File.fnmatch(pattern, source) } - end - else - sources - end - end - # Private: append a nonce to the script/style directories if script_nonce # or style_nonce are provided. def populate_nonces(directive, source_list) From cd73b021880bb368317b780ed6ac6d1841462574 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Wed, 19 Oct 2022 05:26:41 -0700 Subject: [PATCH 592/636] Update tests. --- .../headers/content_security_policy_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index b3e64c92..314b8963 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -48,12 +48,12 @@ module SecureHeaders expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:") end - it "minifies source expressions based on overlapping wildcards" do + it "does not minify source expressions based on overlapping wildcards" do config = { default_src: %w(a.example.org b.example.org *.example.org https://*.example.org) } csp = ContentSecurityPolicy.new(config) - expect(csp.value).to eq("default-src *.example.org") + expect(csp.value).to eq("default-src a.example.org b.example.org *.example.org") end it "removes http/s schemes from hosts" do @@ -106,8 +106,8 @@ module SecureHeaders expect(csp.value).to eq("default-src *.example.org:*") end - it "deduplicates any source expressions" do - csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org)) + it "deduplicates source expressions that match exactly (after scheme stripping)" do + csp = ContentSecurityPolicy.new(default_src: %w(example.org https://example.org example.org)) expect(csp.value).to eq("default-src example.org") end From 3adb9ba26c1a648f3c6c3c7755cab1c3e1b89371 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Mon, 24 Oct 2022 12:05:20 -0700 Subject: [PATCH 593/636] v6.5.0 Release notes: - CSP: Remove source expression deduplication. (@lgarron) https://github.com/github/secure_headers/pull/499 --- CHANGELOG.md | 4 ++++ lib/secure_headers/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd0ce3a..662bdd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.5.0 + +- CSP: Remove source expression deduplication. (@lgarron) https://github.com/github/secure_headers/pull/499 + ## 6.4.0 - CSP: Add support for trusted-types, require-trusted-types-for directive (@JackMc): https://github.com/github/secure_headers/pull/486 diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 2614e5fe..b65e0a07 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.4.0" + VERSION = "6.5.0" end From bb167fa2f1ffbe99d5bffed3a720fa4b234553dc Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Mon, 24 Oct 2022 20:30:40 -0700 Subject: [PATCH 594/636] Update `.ruby-version` to `3.1.1` I'm having trouble using `2.6.6`, so it would be more straightforward to use a newer version by default. --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 338a5b5d..94ff29cc 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.6 +3.1.1 From 7733738c991669d494594d08f54926efc221080d Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Mon, 26 Dec 2022 16:45:17 -0500 Subject: [PATCH 595/636] Adds Ruby 3.2 to the CI matrix. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3588214c..c57020c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: [ '2.6', '2.7', '3.0', '3.1' ] + ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2' ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby ${{ matrix.ruby }} uses: ruby/setup-ruby@v1 with: From 55799c222b7a1040241f850f7b53bfd78a166eff Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Mon, 26 Dec 2022 16:48:24 -0500 Subject: [PATCH 596/636] Fix lints --- lib/secure_headers/headers/cookie.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 0ff045c2..9d3f7cd1 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -80,9 +80,9 @@ def flag_cookie?(attribute) end def conditionally_flag?(configuration) - if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?) + if (Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?) true - elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?) + elsif (Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?) true else false From ff9797fe967c85605ed576dac236b74776306c05 Mon Sep 17 00:00:00 2001 From: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Date: Wed, 19 Jul 2023 07:32:07 -0400 Subject: [PATCH 597/636] deprecate block-all-mixed-content (#509) --- README.md | 4 +++- .../headers/content_security_policy_config.rb | 1 - lib/secure_headers/headers/policy_management.rb | 5 +---- .../headers/content_security_policy_spec.rb | 8 ++++---- .../headers/policy_management_spec.rb | 15 ++++----------- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b842cac4..a43004b5 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ SecureHeaders::Configuration.default do |config| # directive values: these values will directly translate into source directives default_src: %w('none'), base_uri: %w('self'), - block_all_mixed_content: true, # see https://www.w3.org/TR/mixed-content/ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. connect_src: %w(wss:), font_src: %w('self' data:), @@ -92,6 +91,9 @@ SecureHeaders::Configuration.default do |config| end ``` +### Deprecated Configuration Values +* `block_all_mixed_content` - this value is deprecated in favor of `upgrade_insecure_requests`. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/block-all-mixed-content for more information. + ## Default values All headers except for PublicKeyPins and ClearSiteData have a default value. The default set of headers is: diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 5d3c7550..5f1dbb57 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -16,7 +16,6 @@ def self.included(base) def initialize(hash) @base_uri = nil - @block_all_mixed_content = nil @child_src = nil @connect_src = nil @default_src = nil diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 1d464f4c..668e79a5 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -71,7 +71,6 @@ def self.included(base) # All the directives currently under consideration for CSP level 3. # https://w3c.github.io/webappsec/specs/CSP2/ - BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content MANIFEST_SRC = :manifest_src NAVIGATE_TO = :navigate_to PREFETCH_SRC = :prefetch_src @@ -85,7 +84,6 @@ def self.included(base) DIRECTIVES_3_0 = [ DIRECTIVES_2_0, - BLOCK_ALL_MIXED_CONTENT, MANIFEST_SRC, NAVIGATE_TO, PREFETCH_SRC, @@ -118,7 +116,6 @@ def self.included(base) DIRECTIVE_VALUE_TYPES = { BASE_URI => :source_list, - BLOCK_ALL_MIXED_CONTENT => :boolean, CHILD_SRC => :source_list, CONNECT_SRC => :source_list, DEFAULT_SRC => :source_list, @@ -241,7 +238,7 @@ def validate_config!(config) # # raises an error if the original config is OPT_OUT # - # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests), + # 1. for non-source-list values (report_only, upgrade_insecure_requests), # additions will overwrite the original value. # 2. if a value in additions does not exist in the original config, the # default-src value is included to match original behavior. diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 314b8963..37cb62a7 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -92,13 +92,13 @@ module SecureHeaders end it "does add a boolean directive if the value is true" do - csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: true) - expect(csp.value).to eq("default-src example.org; block-all-mixed-content; upgrade-insecure-requests") + csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], upgrade_insecure_requests: true) + expect(csp.value).to eq("default-src example.org; upgrade-insecure-requests") end it "does not add a boolean directive if the value is false" do - csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: false) - expect(csp.value).to eq("default-src example.org; block-all-mixed-content") + csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], upgrade_insecure_requests: false) + expect(csp.value).to eq("default-src example.org") end it "handles wildcard subdomain with wildcard port" do diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 0f7cf8a8..c621e88e 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -30,7 +30,6 @@ module SecureHeaders default_src: %w(https: 'self'), base_uri: %w('self'), - block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) connect_src: %w(wss:), child_src: %w('self' *.twimg.com itunes.apple.com), font_src: %w('self' data:), @@ -92,12 +91,6 @@ module SecureHeaders end.to raise_error(ContentSecurityPolicyConfigError) end - it "requires :block_all_mixed_content to be a boolean value" do - expect do - ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve"))) - end.to raise_error(ContentSecurityPolicyConfigError) - end - it "requires :upgrade_insecure_requests to be a boolean value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve"))) @@ -244,18 +237,18 @@ module SecureHeaders expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) end - it "overrides the :block_all_mixed_content flag" do + it "overrides the :upgrade_insecure_requests flag" do Configuration.default do |config| config.csp = { default_src: %w(https:), script_src: %w('self'), - block_all_mixed_content: false + upgrade_insecure_requests: false } end default_policy = Configuration.dup - combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, block_all_mixed_content: true) + combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, upgrade_insecure_requests: true) csp = ContentSecurityPolicy.new(combined_config) - expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'") + expect(csp.value).to eq("default-src https:; script-src 'self'; upgrade-insecure-requests") end it "raises an error if appending to a OPT_OUT policy" do From 7a23cb6b350b024a786e163e81c902552b9c484f Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Fri, 11 Aug 2023 11:20:28 -0700 Subject: [PATCH 598/636] Make SecureSecurityPolicyConfig significantly faster (#506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have been seeing this gem a lot in profiles. Must of this slowness seems to come from overuse of instance variables in `DynamicConfig` and attempting to use them basically as a hash (which we can do much faster with a hash 😅) The first commit of these is the most important, but the other two also significantly speed things up. There is definitely more improvement available here, we seem to be overly cautious in duplicating arrays, and we also seem to convert unnecessarily between hashes and the config object, but I think this is the best place to start.
Benchmark: ``` require "secure_headers" require "benchmark/ips" # Copied from README MyCSPConfig = SecureHeaders::ContentSecurityPolicyConfig.new( preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. disable_nonce_backwards_compatibility: true, # default: false. If false, `unsafe-inline` will be added automatically when using nonces. If true, it won't. See #403 for why you'd want this. # directive values: these values will directly translate into source directives default_src: %w('none'), base_uri: %w('self'), block_all_mixed_content: true, # see https://www.w3.org/TR/mixed-content/ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. connect_src: %w(wss:), font_src: %w('self' data:), form_action: %w('self' github.com), frame_ancestors: %w('none'), img_src: %w(mycdn.com data:), manifest_src: %w('self'), media_src: %w(utoob.com), object_src: %w('self'), sandbox: true, # true and [] will set a maximally restrictive setting plugin_types: %w(application/x-shockwave-flash), script_src: %w('self'), script_src_elem: %w('self'), script_src_attr: %w('self'), style_src: %w('unsafe-inline'), style_src_elem: %w('unsafe-inline'), style_src_attr: %w('unsafe-inline'), worker_src: %w('self'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) ) Benchmark.ips do |x| x.report "csp_config.to_h" do MyCSPConfig.to_h end x.report "csp_config.append" do MyCSPConfig.append({}) end x.report "new(config).value" do SecureHeaders::ContentSecurityPolicy.new(MyCSPConfig).value end end ```
**Before:** ``` $ be ruby bench.rb Warming up -------------------------------------- csp_config.to_h 13.737k i/100ms csp_config.append 2.105k i/100ms new(config).value 1.429k i/100ms Calculating ------------------------------------- csp_config.to_h 139.988k (± 0.3%) i/s - 700.587k in 5.004666s csp_config.append 21.133k (± 2.4%) i/s - 107.355k in 5.082856s new(config).value 14.298k (± 0.4%) i/s - 72.879k in 5.097116s ``` **After:** ``` $ be ruby bench.rb Warming up -------------------------------------- csp_config.to_h 123.784k i/100ms csp_config.append 4.181k i/100ms new(config).value 1.617k i/100ms Calculating ------------------------------------- csp_config.to_h 1.238M (± 3.1%) i/s - 6.189M in 5.003769s csp_config.append 40.921k (± 1.0%) i/s - 204.869k in 5.006924s new(config).value 16.095k (± 0.4%) i/s - 80.850k in 5.023259s ``` `to_h` is 10x faster, `append` is 2x faster, and .value (which was not the target of these optimizations but I didn't want to see it regress) is slightly faster --------- Co-authored-by: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> --- Gemfile | 2 + lib/secure_headers/configuration.rb | 9 ++- .../headers/content_security_policy.rb | 6 +- .../headers/content_security_policy_config.rb | 72 ++++--------------- 4 files changed, 26 insertions(+), 63 deletions(-) diff --git a/Gemfile b/Gemfile index 78cca83b..007b8f12 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,8 @@ source "https://rubygems.org" gemspec +gem "benchmark-ips" + group :test do gem "coveralls" gem "json" diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 996ca74f..2ebbf487 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -83,14 +83,17 @@ def named_append_or_override_exists?(name) # can lead to modifying parent objects. def deep_copy(config) return unless config - config.each_with_object({}) do |(key, value), hash| - hash[key] = - if value.is_a?(Array) + result = {} + config.each_pair do |key, value| + result[key] = + case value + when Array value.dup else value end end + result end # Private: Returns the internal default configuration. This should only diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 566004b8..4a7b0d76 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -20,9 +20,9 @@ def initialize(config = nil) config end - @preserve_schemes = @config.preserve_schemes - @script_nonce = @config.script_nonce - @style_nonce = @config.style_nonce + @preserve_schemes = @config[:preserve_schemes] + @script_nonce = @config[:script_nonce] + @style_nonce = @config[:style_nonce] end ## diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 5f1dbb57..50ca6bd4 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -1,65 +1,23 @@ # frozen_string_literal: true module SecureHeaders module DynamicConfig - def self.included(base) - base.send(:attr_reader, *base.attrs) - base.attrs.each do |attr| - base.send(:define_method, "#{attr}=") do |value| - if self.class.attrs.include?(attr) - write_attribute(attr, value) - else - raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}" - end - end - end - end - def initialize(hash) - @base_uri = nil - @child_src = nil - @connect_src = nil - @default_src = nil - @font_src = nil - @form_action = nil - @frame_ancestors = nil - @frame_src = nil - @img_src = nil - @manifest_src = nil - @media_src = nil - @navigate_to = nil - @object_src = nil - @plugin_types = nil - @prefetch_src = nil - @preserve_schemes = nil - @report_only = nil - @report_uri = nil - @require_sri_for = nil - @require_trusted_types_for = nil - @sandbox = nil - @script_nonce = nil - @script_src = nil - @script_src_elem = nil - @script_src_attr = nil - @style_nonce = nil - @style_src = nil - @style_src_elem = nil - @style_src_attr = nil - @trusted_types = nil - @worker_src = nil - @upgrade_insecure_requests = nil - @disable_nonce_backwards_compatibility = nil + @config = {} from_hash(hash) end + def initialize_copy(hash) + @config = hash.to_h + end + def update_directive(directive, value) - self.send("#{directive}=", value) + @config[directive] = value end def directive_value(directive) - if self.class.attrs.include?(directive) - self.send(directive) - end + # No need to check attrs, as we only assign valid keys + @config[directive] end def merge(new_hash) @@ -77,10 +35,7 @@ def append(new_hash) end def to_h - self.class.attrs.each_with_object({}) do |key, hash| - value = self.send(key) - hash[key] = value unless value.nil? - end + @config.dup end def dup @@ -113,8 +68,11 @@ def from_hash(hash) def write_attribute(attr, value) value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list - attr_variable = "@#{attr}" - self.instance_variable_set(attr_variable, value) + if value.nil? + @config.delete(attr) + else + @config[attr] = value + end end end @@ -122,7 +80,7 @@ class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicyConfig HEADER_NAME = "Content-Security-Policy".freeze - ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES + ATTRS = Set.new(PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES) def self.attrs ATTRS end From f85f6312099b5cbc160368470d52a558f0bc1a46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 15:09:27 +0000 Subject: [PATCH 599/636] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c57020c0..071da11a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2' ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} uses: ruby/setup-ruby@v1 with: From b122c2fa39bb626be7957c3cf8811d10feb234af Mon Sep 17 00:00:00 2001 From: Brandon Stewart <20469703+boveus@users.noreply.github.com> Date: Thu, 8 Aug 2024 19:55:29 -0400 Subject: [PATCH 600/636] Update build.yml (#520) ## All PRs: * [x] Has tests N/A * [x] Documentation updated N/A ## Adding a new header Generally, adding a new header is always OK. * Is the header supported by any user agent? If so, which? * What does it do? * What are the valid values for the header? * Where does the specification live? ## Adding a new CSP directive * Is the directive supported by any user agent? If so, which? * What does it do? * What are the valid values for the directive? --------- Co-authored-by: Rahul Zhade --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 071da11a..67827e1c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,10 +15,8 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} + bundler-cache: true - name: Build and test with Rake run: | - gem install bundler - bundle install --jobs 4 --retry 3 --without guard - bundle exec rspec spec bundle exec rubocop - + bundle exec rspec spec From cf56fc9182ed2c23e7c0d99da1ff8e7a4659bedd Mon Sep 17 00:00:00 2001 From: Rahul Zhade Date: Thu, 8 Aug 2024 20:09:11 -0400 Subject: [PATCH 601/636] Update default X-XSS-Protection value to 0 (#479) This PR updates the default value of the `X-XSS-Protection` header to 0. There's further discussion here about the reasons for this: https://github.com/github/secure_headers/issues/439. ## All PRs: * [x] Has tests * [x] Documentation updated Closes https://github.com/github/secure_headers/issues/439 --- README.md | 2 +- lib/secure_headers/headers/x_xss_protection.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a43004b5..1683c84f 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ X-Content-Type-Options: nosniff X-Download-Options: noopen X-Frame-Options: sameorigin X-Permitted-Cross-Domain-Policies: none -X-Xss-Protection: 1; mode=block +X-Xss-Protection: 0 ``` ## API configurations diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index 27a99c02..5be385de 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -3,7 +3,7 @@ module SecureHeaders class XXssProtectionConfigError < StandardError; end class XXssProtection HEADER_NAME = "X-XSS-Protection".freeze - DEFAULT_VALUE = "1; mode=block" + DEFAULT_VALUE = "0".freeze VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/ class << self From 706d66e80d8f77cbbb8012856d27a89a5c137cda Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 9 Aug 2024 11:58:58 -0400 Subject: [PATCH 602/636] Add permissions to build workflow and pin ruby --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67827e1c..c6b26a18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,9 @@ name: Build + Test on: [pull_request, push] +permissions: + contents: read + jobs: build: name: Build + Test @@ -12,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 #v1.190.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 8e548ee562460b3978b0e4e12727ad0188a69db8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:08:56 -0400 Subject: [PATCH 603/636] Bump ruby/setup-ruby from 1.190.0 to 1.195.0 (#526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.190.0 to 1.195.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.195.0

What's Changed

New Contributors

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.194.0...v1.195.0

v1.194.0

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.193.0...v1.194.0

v1.193.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.192.0...v1.193.0

v1.192.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.191.0...v1.192.0

v1.191.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.190.0...v1.191.0

Commits
  • 086ffb1 Update the dist/index.js.
  • 27ceac8 Add asan to the test matrix.
  • aac0c64 Add asan as a head version of Ruby.
  • 727fd4f Update ruby-builder-versions.json
  • 79c01b8 README.md: Mention link to supported Ruby versions for GitHub-hosted runners
  • c04af2b Replace macos-release by (parseInt(os.version()) - 9)
  • 17f5c91 Add support for macos-15 and all future macos runners
  • 5496baa Update macos-release to 3.3.0 so it recognizes macos-15
  • 0ec4904 Better error for TruffleRuby on Windows
  • f321cf5 Add truffleruby-24.1.0,truffleruby+graalvm-24.1.0
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.190.0&new-version=1.195.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6b26a18..0f2175d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 #v1.190.0 tag + uses: ruby/setup-ruby@086ffb1a2090c870a3f881cc91ea83aa4243d408 #v1.190.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 6b5eb3304ddb073d19e02ea1bc64c8a0bcb37b21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:42:38 -0400 Subject: [PATCH 604/636] Bump ruby/setup-ruby from 1.195.0 to 1.196.0 (#527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.195.0 to 1.196.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.196.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.195.0...v1.196.0

Commits
  • f269373 Test the last 3.4 preview in CI
  • 784fcda Add ruby-3.4.0-preview2
  • f6e0571 Update README.md
  • 207a399 Move the link to the list together with other text about builds
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.195.0&new-version=1.196.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f2175d3..cceafed6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@086ffb1a2090c870a3f881cc91ea83aa4243d408 #v1.190.0 tag + uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 #v1.190.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From bac6daea1172745eed5370d316726e762b574c92 Mon Sep 17 00:00:00 2001 From: Rahul Zhade Date: Wed, 16 Oct 2024 13:18:26 -0400 Subject: [PATCH 605/636] Upgrade version and docs to 7.0 (#528) ## All PRs: * [x] Has tests * [x] Documentation updated ## Adding a new header N/A ## Adding a new CSP directive N/A Closes https://github.com/github/secure_headers/issues/480 --- docs/upgrading-to-7-0.md | 12 ++++++++++++ lib/secure_headers/version.rb | 2 +- secure_headers.gemspec | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/upgrading-to-7-0.md diff --git a/docs/upgrading-to-7-0.md b/docs/upgrading-to-7-0.md new file mode 100644 index 00000000..38e93513 --- /dev/null +++ b/docs/upgrading-to-7-0.md @@ -0,0 +1,12 @@ +## X-Xss-Protection is set to 0 by default + +Version 6 and below of `secure_headers` set the `X-Xss-Protection` to `1; mode=block` by default. This was done to protect against reflected XSS attacks. However, this header is no longer recommended (see https://github.com/github/secure_headers/issues/439 for more information). + +If any functionality in your app depended on this header being set to the previous value, you will need to set it explicitly in your configuration. + +```ruby +# config/initializers/secure_headers.rb +SecureHeaders::Configuration.default do |config| + config.x_xss_protection = "1; mode=block" +end +``` \ No newline at end of file diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index b65e0a07..a0286e39 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "6.5.0" + VERSION = "7.0.0" end diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 4880c3ff..af0e2187 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |gem| gem.description = 'Add easily configured security headers to responses including content-security-policy, x-frame-options, strict-transport-security, etc.' - gem.homepage = "https://github.com/twitter/secureheaders" + gem.homepage = "https://github.com/github/secure_headers" gem.license = "MIT" gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } From b134eef07d3741b4bd0769b863961b41af5df57d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:09:59 -0400 Subject: [PATCH 606/636] Bump ruby/setup-ruby from 1.196.0 to 1.197.0 (#530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.196.0 to 1.197.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.197.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.196.0...v1.197.0

Commits
  • 7bae1d0 Add truffleruby-24.1.1,truffleruby+graalvm-24.1.1
  • 4d521ea Fix exclusion
  • 1d9686e Mention the new ruby-asan under Supported Versions
  • bea2fb9 Add some details about using asan build.
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.196.0&new-version=1.197.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cceafed6..5f43054c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 #v1.190.0 tag + uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 #v1.190.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From ed53da2323a9c9345930f50c0e9d9f3b45ee7c66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:08:37 -0500 Subject: [PATCH 607/636] Bump ruby/setup-ruby from 1.197.0 to 1.202.0 (#535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.197.0 to 1.202.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.202.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.201.0...v1.202.0

v1.201.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.200.0...v1.201.0

v1.200.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.199.0...v1.200.0

v1.199.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.198.0...v1.199.0

v1.198.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.197.0...v1.198.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.197.0&new-version=1.202.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f43054c..289700b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 #v1.190.0 tag + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc #v1.190.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 586681068324339674fffd1f2b85c613cc8082bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:14:31 -0500 Subject: [PATCH 608/636] Bump ruby/setup-ruby from 1.202.0 to 1.203.0 (#537) Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.202.0 to 1.203.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.203.0

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.202.0...v1.203.0

Commits
  • 2a18b06 Require macos-13+ for macos-amd64 as builds from now on will be on 13 and 12 ...
  • 2e3487c No more macos-12 runners, minimum for macos-amd64 is now macos-13
  • 6825c76 Clarify
  • 9a9fe58 Clarify all macOS versions from 12 are supported
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.202.0&new-version=1.203.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 289700b3..c13c2a2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc #v1.190.0 tag + uses: ruby/setup-ruby@2a18b06812b0e15bb916e1df298d3e740422c47e #v1.190.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 6d583bd193e26bb9074157fbcdab8fa14b367e43 Mon Sep 17 00:00:00 2001 From: Rahul Zhade Date: Mon, 16 Dec 2024 12:19:28 -0500 Subject: [PATCH 609/636] Cleanup Repository files and Gem build (#536) This PR updates this repository to remove extraneous files, update the README.md, and remove files from the Gem build. Closes https://github.com/github/secure_headers/issues/531 --- .ruby-gemset | 1 - .ruby-version | 2 +- README.md | 2 +- secure_headers.gemspec | 16 +++++++++++++--- 4 files changed, 15 insertions(+), 6 deletions(-) delete mode 100644 .ruby-gemset diff --git a/.ruby-gemset b/.ruby-gemset deleted file mode 100644 index 5a6c467b..00000000 --- a/.ruby-gemset +++ /dev/null @@ -1 +0,0 @@ -secureheaders diff --git a/.ruby-version b/.ruby-version index 94ff29cc..8a4b2758 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.1 +3.1.6 \ No newline at end of file diff --git a/README.md b/README.md index 1683c84f..598cdd59 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=main) -**main branch represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. +**main branch represents 7.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), [upgrading to 6.x doc](docs/upgrading-to-6-0.md) or [upgrading to 7.x doc](docs/upgrading-to-7-0.md) for instructions on how to upgrade. Bug fixes should go in the `6.x` branch for now. The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](https://www.w3.org/TR/CSP2/) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index af0e2187..f8b6c5b9 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -14,10 +14,20 @@ Gem::Specification.new do |gem| including content-security-policy, x-frame-options, strict-transport-security, etc.' gem.homepage = "https://github.com/github/secure_headers" + gem.metadata = { + "bug_tracker_uri" => "https://github.com/github/secure_headers/issues", + "changelog_uri" => "https://github.com/github/secure_headers/blob/master/CHANGELOG.md", + "documentation_uri" => "https://rubydoc.info/gems/secure_headers", + "homepage_uri" => gem.homepage, + "source_code_uri" => "https://github.com/github/secure_headers", + "rubygems_mfa_required" => "true", + } gem.license = "MIT" - gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) - gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } - gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + + gem.files = Dir["bin/**/*", "lib/**/*", "README.md", "CHANGELOG.md", "LICENSE", "Gemfile", "secure_headers.gemspec"] gem.require_paths = ["lib"] + + gem.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE"] + gem.add_development_dependency "rake" end From b91d1d91531a0e8a0e3a3da13c04598929581f84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:19:51 -0500 Subject: [PATCH 610/636] Bump ruby/setup-ruby from 1.203.0 to 1.204.0 (#538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.203.0 to 1.204.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.204.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.203.0...v1.204.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.203.0&new-version=1.204.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c13c2a2b..62346520 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@2a18b06812b0e15bb916e1df298d3e740422c47e #v1.190.0 tag + uses: ruby/setup-ruby@401c19e14f474b54450cd3905bb8b86e2c8509cf #v1.190.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 5ff1d1a8b690ad0253bb233c8f549e61ee3816fd Mon Sep 17 00:00:00 2001 From: Arash Naveed Date: Tue, 17 Dec 2024 00:11:30 +0500 Subject: [PATCH 611/636] Lowercase headers (#533) Rack 3 `Response header keys can no longer include uppercase characters.` https://github.com/rack/rack/blob/main/UPGRADE-GUIDE.md#rack-3-upgrade-guide Rack Lint error out when using `secure_headers` because its adding headers with uppercase ``` Rack::Lint::LintError: uppercase character in header name: X-Frame-Options (Rack::Lint::LintError) ``` --------- Co-authored-by: Rahul Zhade --- README.md | 24 +++--- docs/hashes.md | 2 +- docs/per_action_configuration.md | 6 +- docs/upgrading-to-4-0.md | 2 +- lib/secure_headers/configuration.rb | 2 +- lib/secure_headers/headers/clear_site_data.rb | 8 +- .../headers/content_security_policy.rb | 4 +- .../headers/content_security_policy_config.rb | 4 +- .../expect_certificate_transparency.rb | 4 +- .../headers/policy_management.rb | 4 +- lib/secure_headers/headers/referrer_policy.rb | 2 +- .../headers/strict_transport_security.rb | 2 +- .../headers/x_content_type_options.rb | 2 +- .../headers/x_download_options.rb | 4 +- lib/secure_headers/headers/x_frame_options.rb | 2 +- .../x_permitted_cross_domain_policies.rb | 4 +- .../headers/x_xss_protection.rb | 2 +- lib/secure_headers/railtie.rb | 10 +-- spec/lib/secure_headers_spec.rb | 84 +++++++++++-------- 19 files changed, 94 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 598cdd59..114cb7b4 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ The gem will automatically apply several headers that are related to security. - X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034) - X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx) - X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx) -- X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx) -- X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) -- Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/) -- Expect-CT - Only use certificates that are present in the certificate transparency logs. [Expect-CT draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/). -- Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/). +- x-download-options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx) +- x-permitted-cross-domain-policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) +- referrer-policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/) +- expect-ct - Only use certificates that are present in the certificate transparency logs. [expect-ct draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/). +- clear-site-data - Clearing browser data for origin. [clear-site-data specification](https://w3c.github.io/webappsec-clear-site-data/). It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes. This is on default but can be turned off by using `config.cookies = SecureHeaders::OPT_OUT`. @@ -99,13 +99,13 @@ end All headers except for PublicKeyPins and ClearSiteData have a default value. The default set of headers is: ``` -Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' -Strict-Transport-Security: max-age=631138519 -X-Content-Type-Options: nosniff -X-Download-Options: noopen -X-Frame-Options: sameorigin -X-Permitted-Cross-Domain-Policies: none -X-Xss-Protection: 0 +content-security-policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' +strict-transport-security: max-age=631138519 +x-content-type-options: nosniff +x-download-options: noopen +x-frame-options: sameorigin +x-permitted-cross-domain-policies: none +x-xss-protection: 0 ``` ## API configurations diff --git a/docs/hashes.md b/docs/hashes.md index 752d42a5..ae6af984 100644 --- a/docs/hashes.md +++ b/docs/hashes.md @@ -58,7 +58,7 @@ console.log(1) ``` ``` -Content-Security-Policy: ... +content-security-policy: ... script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; ``` diff --git a/docs/per_action_configuration.md b/docs/per_action_configuration.md index 63716dba..fda5d59a 100644 --- a/docs/per_action_configuration.md +++ b/docs/per_action_configuration.md @@ -91,7 +91,7 @@ body { ``` -Content-Security-Policy: ... +content-security-policy: ... script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; ``` @@ -118,13 +118,13 @@ You can clear the browser cache after the logout request by using the following. ``` ruby class ApplicationController < ActionController::Base - # Configuration override to send the Clear-Site-Data header. + # Configuration override to send the clear-site-data header. SecureHeaders::Configuration.override(:clear_browser_cache) do |config| config.clear_site_data = SecureHeaders::ClearSiteData::ALL_TYPES end - # Clears the browser's cache for browsers supporting the Clear-Site-Data + # Clears the browser's cache for browsers supporting the clear-site-data # header. # # Returns nothing. diff --git a/docs/upgrading-to-4-0.md b/docs/upgrading-to-4-0.md index f9d6a2bb..15da15af 100644 --- a/docs/upgrading-to-4-0.md +++ b/docs/upgrading-to-4-0.md @@ -15,7 +15,7 @@ The default CSP has changed to be more universal without sacrificing too much se Previously, the default CSP was: -`Content-Security-Policy: default-src 'self'` +`content-security-policy: default-src 'self'` The new default policy is: diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 2ebbf487..e96f4f9d 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -256,7 +256,7 @@ def csp=(new_csp) end end - # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot + # Configures the content-security-policy-report-only header. `new_csp` cannot # contain `report_only: false` or an error will be raised. # # NOTE: if csp has not been configured/has the default value when diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index bf8078d4..0fdc0c2a 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -2,7 +2,7 @@ module SecureHeaders class ClearSiteDataConfigError < StandardError; end class ClearSiteData - HEADER_NAME = "Clear-Site-Data".freeze + HEADER_NAME = "clear-site-data".freeze # Valid `types` CACHE = "cache".freeze @@ -12,7 +12,7 @@ class ClearSiteData ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS] class << self - # Public: make an Clear-Site-Data header name, value pair + # Public: make an clear-site-data header name, value pair # # Returns nil if not configured, returns header name and value if configured. def make_header(config = nil, user_agent = nil) @@ -39,8 +39,8 @@ def validate_config!(config) end end - # Public: Transform a Clear-Site-Data config (an Array of Strings) into a - # String that can be used as the value for the Clear-Site-Data header. + # Public: Transform a clear-site-data config (an Array of Strings) into a + # String that can be used as the value for the clear-site-data header. # # types - An Array of String of types of data to clear. # diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 4a7b0d76..ae225e7c 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -26,8 +26,8 @@ def initialize(config = nil) end ## - # Returns the name to use for the header. Either "Content-Security-Policy" or - # "Content-Security-Policy-Report-Only" + # Returns the name to use for the header. Either "content-security-policy" or + # "content-security-policy-report-only" def name @config.class.const_get(:HEADER_NAME) end diff --git a/lib/secure_headers/headers/content_security_policy_config.rb b/lib/secure_headers/headers/content_security_policy_config.rb index 50ca6bd4..1786b3fa 100644 --- a/lib/secure_headers/headers/content_security_policy_config.rb +++ b/lib/secure_headers/headers/content_security_policy_config.rb @@ -78,7 +78,7 @@ def write_attribute(attr, value) class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicyConfig - HEADER_NAME = "Content-Security-Policy".freeze + HEADER_NAME = "content-security-policy".freeze ATTRS = Set.new(PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES) def self.attrs @@ -107,7 +107,7 @@ def make_report_only end class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig - HEADER_NAME = "Content-Security-Policy-Report-Only".freeze + HEADER_NAME = "content-security-policy-report-only".freeze def report_only? true diff --git a/lib/secure_headers/headers/expect_certificate_transparency.rb b/lib/secure_headers/headers/expect_certificate_transparency.rb index b65cbd09..4b7272dd 100644 --- a/lib/secure_headers/headers/expect_certificate_transparency.rb +++ b/lib/secure_headers/headers/expect_certificate_transparency.rb @@ -3,14 +3,14 @@ module SecureHeaders class ExpectCertificateTransparencyConfigError < StandardError; end class ExpectCertificateTransparency - HEADER_NAME = "Expect-CT".freeze + HEADER_NAME = "expect-ct".freeze INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze class << self - # Public: Generate a Expect-CT header. + # Public: Generate a expect-ct header. # # Returns nil if not configured, returns header name and value if # configured. diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 668e79a5..3129c0d3 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -98,9 +98,9 @@ def self.included(base) # Experimental directives - these vary greatly in support # See MDN for details. - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-security-policy/trusted-types TRUSTED_TYPES = :trusted_types - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-security-policy/require-trusted-types-for REQUIRE_TRUSTED_TYPES_FOR = :require_trusted_types_for DIRECTIVES_EXPERIMENTAL = [ diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index c18188f8..a4589117 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -2,7 +2,7 @@ module SecureHeaders class ReferrerPolicyConfigError < StandardError; end class ReferrerPolicy - HEADER_NAME = "Referrer-Policy".freeze + HEADER_NAME = "referrer-policy".freeze DEFAULT_VALUE = "origin-when-cross-origin" VALID_POLICIES = %w( no-referrer diff --git a/lib/secure_headers/headers/strict_transport_security.rb b/lib/secure_headers/headers/strict_transport_security.rb index 36df032c..3d78a484 100644 --- a/lib/secure_headers/headers/strict_transport_security.rb +++ b/lib/secure_headers/headers/strict_transport_security.rb @@ -3,7 +3,7 @@ module SecureHeaders class STSConfigError < StandardError; end class StrictTransportSecurity - HEADER_NAME = "Strict-Transport-Security".freeze + HEADER_NAME = "strict-transport-security".freeze HSTS_MAX_AGE = "631138519" DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index cf4371f5..96f8d314 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -3,7 +3,7 @@ module SecureHeaders class XContentTypeOptionsConfigError < StandardError; end class XContentTypeOptions - HEADER_NAME = "X-Content-Type-Options".freeze + HEADER_NAME = "x-content-type-options".freeze DEFAULT_VALUE = "nosniff" class << self diff --git a/lib/secure_headers/headers/x_download_options.rb b/lib/secure_headers/headers/x_download_options.rb index db327dca..1eb1356a 100644 --- a/lib/secure_headers/headers/x_download_options.rb +++ b/lib/secure_headers/headers/x_download_options.rb @@ -2,11 +2,11 @@ module SecureHeaders class XDOConfigError < StandardError; end class XDownloadOptions - HEADER_NAME = "X-Download-Options".freeze + HEADER_NAME = "x-download-options".freeze DEFAULT_VALUE = "noopen" class << self - # Public: generate an X-Download-Options header. + # Public: generate an x-download-options header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. diff --git a/lib/secure_headers/headers/x_frame_options.rb b/lib/secure_headers/headers/x_frame_options.rb index 508ae364..636e3bfa 100644 --- a/lib/secure_headers/headers/x_frame_options.rb +++ b/lib/secure_headers/headers/x_frame_options.rb @@ -2,7 +2,7 @@ module SecureHeaders class XFOConfigError < StandardError; end class XFrameOptions - HEADER_NAME = "X-Frame-Options".freeze + HEADER_NAME = "x-frame-options".freeze SAMEORIGIN = "sameorigin" DENY = "deny" ALLOW_FROM = "allow-from" diff --git a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb index 416c9f01..a34dd58f 100644 --- a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +++ b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb @@ -2,12 +2,12 @@ module SecureHeaders class XPCDPConfigError < StandardError; end class XPermittedCrossDomainPolicies - HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze + HEADER_NAME = "x-permitted-cross-domain-policies".freeze DEFAULT_VALUE = "none" VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) class << self - # Public: generate an X-Permitted-Cross-Domain-Policies header. + # Public: generate an x-permitted-cross-domain-policies header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index 5be385de..bd5b7faa 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -2,7 +2,7 @@ module SecureHeaders class XXssProtectionConfigError < StandardError; end class XXssProtection - HEADER_NAME = "X-XSS-Protection".freeze + HEADER_NAME = "x-xss-protection".freeze DEFAULT_VALUE = "0".freeze VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/ diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index 53c3aec6..ba255acc 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -4,11 +4,11 @@ module SecureHeaders class Railtie < Rails::Railtie isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0 - conflicting_headers = ["X-Frame-Options", "X-XSS-Protection", - "X-Permitted-Cross-Domain-Policies", "X-Download-Options", - "X-Content-Type-Options", "Strict-Transport-Security", - "Content-Security-Policy", "Content-Security-Policy-Report-Only", - "Public-Key-Pins", "Public-Key-Pins-Report-Only", "Referrer-Policy"] + conflicting_headers = ["x-frame-options", "x-xss-protection", + "x-permitted-cross-domain-policies", "x-download-options", + "x-content-type-options", "strict-transport-security", + "content-security-policy", "content-security-policy-report-only", + "public-key-pins", "public-key-pins-report-only", "referrer-policy"] initializer "secure_headers.middleware" do Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index be608c74..fd66d487 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -9,6 +9,22 @@ module SecureHeaders let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") } + it "has a HEADER_NAME with no capital letters" do + # Iterate through all headerable attributes + Configuration::HEADERABLE_ATTRIBUTES.each do |attr| + klass = Configuration::CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr] + next if [SecureHeaders::ContentSecurityPolicy].include? klass + expect(klass::HEADER_NAME).to eq(klass::HEADER_NAME.downcase) + end + + # Now to iterate through the CSP and CSP-Report-Only classes, since they're registered differently + klass = SecureHeaders::ContentSecurityPolicyConfig + expect(klass::HEADER_NAME).to eq(klass::HEADER_NAME.downcase) + + klass = SecureHeaders::ContentSecurityPolicyReportOnlyConfig + expect(klass::HEADER_NAME).to eq(klass::HEADER_NAME.downcase) + end + it "raises a NotYetConfiguredError if default has not been set" do expect do SecureHeaders.header_hash_for(request) @@ -45,9 +61,9 @@ module SecureHeaders SecureHeaders.opt_out_of_header(request, :csp_report_only) SecureHeaders.opt_out_of_header(request, :x_content_type_options) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy-Report-Only"]).to be_nil - expect(hash["Content-Security-Policy"]).to be_nil - expect(hash["X-Content-Type-Options"]).to be_nil + expect(hash["content-security-policy-report-only"]).to be_nil + expect(hash["content-security-policy"]).to be_nil + expect(hash["x-content-type-options"]).to be_nil end it "Carries options over when using overrides" do @@ -62,9 +78,9 @@ module SecureHeaders SecureHeaders.use_secure_headers_override(request, :api) hash = SecureHeaders.header_hash_for(request) - expect(hash["X-Download-Options"]).to be_nil - expect(hash["X-Permitted-Cross-Domain-Policies"]).to be_nil - expect(hash["X-Frame-Options"]).to be_nil + expect(hash["x-download-options"]).to be_nil + expect(hash["x-permitted-cross-domain-policies"]).to be_nil + expect(hash["x-frame-options"]).to be_nil end it "Overrides the current default config if default config changes during request" do @@ -81,8 +97,8 @@ module SecureHeaders SecureHeaders.use_secure_headers_override(request, :dynamic_override) hash = SecureHeaders.header_hash_for(request) - expect(hash["X-Content-Type-Options"]).to eq("nosniff") - expect(hash["X-Frame-Options"]).to eq("DENY") + expect(hash["x-content-type-options"]).to eq("nosniff") + expect(hash["x-frame-options"]).to eq("DENY") end it "allows you to opt out entirely" do @@ -96,7 +112,7 @@ module SecureHeaders expect(hash.count).to eq(0) end - it "allows you to override X-Frame-Options settings" do + it "allows you to override x-frame-options settings" do Configuration.default SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY) hash = SecureHeaders.header_hash_for(request) @@ -240,7 +256,7 @@ module SecureHeaders SecureHeaders.content_security_policy_script_nonce(chrome_request) hash = SecureHeaders.header_hash_for(chrome_request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}' 'unsafe-inline'; style-src 'self'") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}' 'unsafe-inline'; style-src 'self'") end it "does not support the deprecated `report_only: true` format" do @@ -291,8 +307,8 @@ module SecureHeaders end hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self'") end it "sets different headers when the configs are different" do @@ -306,8 +322,8 @@ module SecureHeaders end hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src foo.com") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src foo.com") end it "allows you to opt-out of enforced CSP" do @@ -321,50 +337,50 @@ module SecureHeaders end hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to be_nil - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["content-security-policy"]).to be_nil + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self'") end it "allows appending to the enforced policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self'") end it "allows appending to the report only policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows appending to both policies" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows overriding the enforced policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self'") end it "allows overriding the report only policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self'") + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src anothercdn.com") end it "allows overriding both policies" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src anothercdn.com") + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src anothercdn.com") end context "when inferring which config to modify" do @@ -379,8 +395,8 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to be_nil + expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["content-security-policy-report-only"]).to be_nil end it "updates the report only header when configured" do @@ -395,8 +411,8 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") - expect(hash["Content-Security-Policy"]).to be_nil + expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") + expect(hash["content-security-policy"]).to be_nil end it "updates both headers if both are configured" do @@ -414,8 +430,8 @@ module SecureHeaders SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) - expect(hash["Content-Security-Policy"]).to eq("default-src enforced.com; script-src 'self' anothercdn.com") - expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src reportonly.com; script-src 'self' anothercdn.com") + expect(hash["content-security-policy"]).to eq("default-src enforced.com; script-src 'self' anothercdn.com") + expect(hash["content-security-policy-report-only"]).to eq("default-src reportonly.com; script-src 'self' anothercdn.com") end end From 10e41fa6361d5674e914499e7903a32ef10834b9 Mon Sep 17 00:00:00 2001 From: Rahul Zhade Date: Mon, 16 Dec 2024 14:19:12 -0500 Subject: [PATCH 612/636] Update version --- lib/secure_headers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index a0286e39..894200a2 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "7.0.0" + VERSION = "7.1.0" end From 8b1029ca6d9a424b903b5beebcd88cbdadf43f4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:25:13 -0500 Subject: [PATCH 613/636] Bump ruby/setup-ruby from 1.204.0 to 1.207.0 (#540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.204.0 to 1.207.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.207.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.206.0...v1.207.0

v1.206.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.205.0...v1.206.0

v1.205.0

What's Changed

New Contributors

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.204.0...v1.205.0

Commits
  • 4a9ddd6 Update CRuby releases on Windows
  • bfefad8 Add ruby-3.4.0,ruby-3.4.1
  • 540484a Fixes the latest rubygems installation error with Ruby 3.0
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.204.0&new-version=1.207.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62346520..840bcb83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@401c19e14f474b54450cd3905bb8b86e2c8509cf #v1.190.0 tag + uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 #v1.190.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From f57f3fdaa268c81278c265452bd2a91ff5a171db Mon Sep 17 00:00:00 2001 From: myersg86 Date: Wed, 5 Feb 2025 22:36:53 -0700 Subject: [PATCH 614/636] fix: Fix typos --- CHANGELOG.md | 4 ++-- lib/secure_headers.rb | 2 +- lib/secure_headers/configuration.rb | 2 +- lib/secure_headers/headers/content_security_policy.rb | 4 ++-- lib/secure_headers/headers/policy_management.rb | 4 ++-- .../headers/x_permitted_cross_domain_policies_spec.rb | 2 +- spec/lib/secure_headers_spec.rb | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 662bdd1a..531894d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ NOTE: this version is a breaking change due to the removal of HPKP. Remove the H ## 5.0.0 -Well this is a little embarassing. 4.0 was supposed to set the secure/httponly/samesite=lax attributes on cookies by default but it didn't. Now it does. - See the [upgrading to 5.0](docs/upgrading-to-5-0.md) guide. +Well this is a little embarrassing. 4.0 was supposed to set the secure/httponly/samesite=lax attributes on cookies by default but it didn't. Now it does. - See the [upgrading to 5.0](docs/upgrading-to-5-0.md) guide. ## 4.0.1 @@ -194,7 +194,7 @@ end ## 3.4.0 the frame-src/child-src transition for Firefox. -Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here: +Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best describes the behavior here: ```ruby if supported_directives.include?(:child_src) diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 6426e538..819087a2 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -178,7 +178,7 @@ def content_security_policy_style_nonce(request) content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC) end - # Public: Retreives the config for a given header type: + # Public: Retrieves the config for a given header type: # # Checks to see if there is an override for this request, then # Checks to see if a named override is used for this request, then diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index e96f4f9d..4fd459ea 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -35,7 +35,7 @@ def default(&block) # Public: create a named configuration that overrides the default config. # - # name - use an idenfier for the override config. + # name - use an identifier for the override config. # base - override another existing config, or override the default config # if no value is supplied. # diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index ae225e7c..055771f0 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -79,7 +79,7 @@ def build_sandbox_list_directive(directive) end # A maximally strict sandbox policy is just the `sandbox` directive, - # whith no configuraiton values. + # with no configuration values. if max_strict_policy symbol_to_hyphen_case(directive) elsif sandbox_list && sandbox_list.any? @@ -120,7 +120,7 @@ def build_source_list_directive(directive) end # If a directive contains *, all other values are omitted. - # If a directive contains 'none' but has other values, 'none' is ommitted. + # If a directive contains 'none' but has other values, 'none' is omitted. # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression) def minify_source_list(directive, source_list) source_list = source_list.compact diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index 3129c0d3..d34c8a8c 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -201,7 +201,7 @@ def make_header(config) # Public: Validates each source expression. # - # Does not validate the invididual values of the source expression (e.g. + # Does not validate the individual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_config!(config) return if config.nil? || config.opt_out? @@ -402,7 +402,7 @@ def validate_require_trusted_types_for_source_expression!(directive, require_tru # 1. is an array of strings # 2. does not contain any deprecated, now invalid values (inline, eval, self, none) # - # Does not validate the invididual values of the source expression (e.g. + # Does not validate the individual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_source_expression!(directive, source_expression) if source_expression != OPT_OUT diff --git a/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb b/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb index 3cd0fa2b..06aa7f94 100644 --- a/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +++ b/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb @@ -37,7 +37,7 @@ module SecureHeaders end end - context "invlaid configuration values" do + context "invalid configuration values" do it "doesn't accept invalid values" do expect do XPermittedCrossDomainPolicies.validate_config!("open") diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index fd66d487..c549ba95 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -226,7 +226,7 @@ module SecureHeaders expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'") end - it "overrides non-existant directives" do + it "overrides non-existent directives" do Configuration.default do |config| config.csp = { default_src: %w(https:), From eb95df4346125cc1a5175ed260def9cfcd058c4c Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 9 Dec 2025 17:35:48 -0500 Subject: [PATCH 615/636] Add explicit dependency to 'cgi' for ruby 4.0 support --- secure_headers.gemspec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index f8b6c5b9..57963290 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -29,5 +29,7 @@ Gem::Specification.new do |gem| gem.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE"] + gem.add_dependency "cgi", "~> 0.5.0" + gem.add_development_dependency "rake" end From 50282a5ed64eafc334ed3b79602cf130fcd83859 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 9 Dec 2025 18:16:04 -0500 Subject: [PATCH 616/636] Support older Ruby versions. --- secure_headers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secure_headers.gemspec b/secure_headers.gemspec index 57963290..5ba8fbb6 100644 --- a/secure_headers.gemspec +++ b/secure_headers.gemspec @@ -29,7 +29,7 @@ Gem::Specification.new do |gem| gem.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE"] - gem.add_dependency "cgi", "~> 0.5.0" + gem.add_dependency "cgi", ">= 0.1" gem.add_development_dependency "rake" end From d9582294adf15c9f33d5f3c9a35dff8f24b9fc17 Mon Sep 17 00:00:00 2001 From: Dennis Pacewicz <119963426+rei-moo@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:49:27 +0000 Subject: [PATCH 617/636] update rubocop configuration for CI --- .rubocop.yml | 19 +++++++++++++++++++ spec/lib/secure_headers/configuration_spec.rb | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 938b22da..7fa59100 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,3 +2,22 @@ inherit_gem: rubocop-github: - config/default.yml require: rubocop-performance + +AllCops: + TargetRubyVersion: 2.6 + +# Disable cops that are not consistently available across all Ruby versions +Style/ClassMethodsDefinitions: + Enabled: false + +Style/OrAssignment: + Enabled: false + +Layout/SpaceInsideHashLiteralBraces: + Enabled: false + +Lint/ParenthesesAsGroupedExpression: + Enabled: false + +Lint/RedundantCopDisableDirective: + Enabled: false diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 73e9b16b..ea3e10e9 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -22,7 +22,9 @@ module SecureHeaders configuration = Configuration.dup expect(original_configuration).not_to be(configuration) Configuration::CONFIG_ATTRIBUTES.each do |attr| - expect(original_configuration.send(attr)).to eq(configuration.send(attr)) + # rubocop:disable GitHub/AvoidObjectSendWithDynamicMethod + expect(original_configuration.public_send(attr)).to eq(configuration.public_send(attr)) + # rubocop:enable GitHub/AvoidObjectSendWithDynamicMethod end end From c1446b16f319c874d7d86d2c8351e8769b247ee5 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 11 Dec 2025 13:32:19 -0500 Subject: [PATCH 618/636] Add ruby 3.4 and 4.0.0-preview2 to CI. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 840bcb83..450e5bb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2' ] + ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2', '3.4', '4.0.0-preview2' ] steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 #v1.190.0 tag + uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c #v1.269.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From d917b0fd3e72d40f7fc45a6613d0ca4dfec0b427 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 11 Dec 2025 13:40:08 -0500 Subject: [PATCH 619/636] Drop ruby 2.6 from CI. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 450e5bb4..6f05512b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2', '3.4', '4.0.0-preview2' ] + ruby: [ '2.7', '3.0', '3.1', '3.2', '3.4', '4.0.0-preview2' ] steps: - uses: actions/checkout@v4 From 82f4fe97e3a5e2d7fc7a0e5e9b042fb4a176529f Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 11 Dec 2025 13:48:13 -0500 Subject: [PATCH 620/636] Generalize to 4.0. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f05512b..2ee26388 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: [ '2.7', '3.0', '3.1', '3.2', '3.4', '4.0.0-preview2' ] + ruby: [ '2.7', '3.0', '3.1', '3.2', '3.4', '4.0' ] steps: - uses: actions/checkout@v4 From 2286b49ee2b45e3bec7cdc55ac5b4ccaf06250c3 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 11 Dec 2025 15:25:38 -0500 Subject: [PATCH 621/636] Fix badge image. The badge image was always showing "unknown". Seems like the URL was wrong, or changed. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 114cb7b4..4682249d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=main) +# Secure Headers [![Build + Test](https://github.com/github/secure_headers/actions/workflows/build.yml/badge.svg)](https://github.com/github/secure_headers/actions/workflows/build.yml) **main branch represents 7.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), [upgrading to 6.x doc](docs/upgrading-to-6-0.md) or [upgrading to 7.x doc](docs/upgrading-to-7-0.md) for instructions on how to upgrade. Bug fixes should go in the `6.x` branch for now. From 33d646198d64a3af33b52385fb21e6809afde793 Mon Sep 17 00:00:00 2001 From: Dennis Pacewicz <119963426+rei-moo@users.noreply.github.com> Date: Sat, 13 Dec 2025 03:49:23 +0900 Subject: [PATCH 622/636] fix linting issues (#563) Inspired by https://github.com/github/secure_headers/pull/558 with the addition of disabling `Lint/RedundantCopDisableDirective` in `.rubocop.yml` in order to prevent rubocop errors related to unknown cops for Ruby versions 2.7 and prior. --------- Co-authored-by: Tobias Maier --- .rubocop.yml | 14 +---- lib/secure_headers.rb | 2 +- lib/secure_headers/headers/clear_site_data.rb | 62 +++++++++---------- lib/secure_headers/headers/cookie.rb | 6 +- .../expect_certificate_transparency.rb | 40 ++++++------ lib/secure_headers/headers/referrer_policy.rb | 40 ++++++------ .../headers/strict_transport_security.rb | 26 ++++---- .../headers/x_content_type_options.rb | 28 ++++----- .../headers/x_download_options.rb | 28 ++++----- lib/secure_headers/headers/x_frame_options.rb | 28 ++++----- .../x_permitted_cross_domain_policies.rb | 28 ++++----- .../headers/x_xss_protection.rb | 26 ++++---- spec/lib/secure_headers/configuration_spec.rb | 6 +- .../headers/content_security_policy_spec.rb | 20 +++--- .../lib/secure_headers/headers/cookie_spec.rb | 10 +-- .../headers/policy_management_spec.rb | 2 +- spec/lib/secure_headers/middleware_spec.rb | 8 +-- spec/lib/secure_headers_spec.rb | 26 ++++---- spec/spec_helper.rb | 18 +++--- 19 files changed, 192 insertions(+), 226 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 7fa59100..27833c9b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,23 +1,11 @@ inherit_gem: rubocop-github: - config/default.yml -require: rubocop-performance +plugins: rubocop-performance AllCops: TargetRubyVersion: 2.6 # Disable cops that are not consistently available across all Ruby versions -Style/ClassMethodsDefinitions: - Enabled: false - -Style/OrAssignment: - Enabled: false - -Layout/SpaceInsideHashLiteralBraces: - Enabled: false - -Lint/ParenthesesAsGroupedExpression: - Enabled: false - Lint/RedundantCopDisableDirective: Enabled: false diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 6426e538..4b288c8e 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -208,7 +208,7 @@ def raise_on_unknown_target(target) def config_and_target(request, target) config = config_for(request) - target = guess_target(config) unless target + target ||= guess_target(config) raise_on_unknown_target(target) [config, target] end diff --git a/lib/secure_headers/headers/clear_site_data.rb b/lib/secure_headers/headers/clear_site_data.rb index 0fdc0c2a..05d26a8f 100644 --- a/lib/secure_headers/headers/clear_site_data.rb +++ b/lib/secure_headers/headers/clear_site_data.rb @@ -11,43 +11,41 @@ class ClearSiteData EXECUTION_CONTEXTS = "executionContexts".freeze ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS] - class << self - # Public: make an clear-site-data header name, value pair - # - # Returns nil if not configured, returns header name and value if configured. - def make_header(config = nil, user_agent = nil) - case config - when nil, OPT_OUT, [] - # noop - when Array - [HEADER_NAME, make_header_value(config)] - when true - [HEADER_NAME, make_header_value(ALL_TYPES)] - end + # Public: make an clear-site-data header name, value pair + # + # Returns nil if not configured, returns header name and value if configured. + def self.make_header(config = nil, user_agent = nil) + case config + when nil, OPT_OUT, [] + # noop + when Array + [HEADER_NAME, make_header_value(config)] + when true + [HEADER_NAME, make_header_value(ALL_TYPES)] end + end - def validate_config!(config) - case config - when nil, OPT_OUT, true - # valid - when Array - unless config.all? { |t| t.is_a?(String) } - raise ClearSiteDataConfigError.new("types must be Strings") - end - else - raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`") + def self.validate_config!(config) + case config + when nil, OPT_OUT, true + # valid + when Array + unless config.all? { |t| t.is_a?(String) } + raise ClearSiteDataConfigError.new("types must be Strings") end + else + raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`") end + end - # Public: Transform a clear-site-data config (an Array of Strings) into a - # String that can be used as the value for the clear-site-data header. - # - # types - An Array of String of types of data to clear. - # - # Returns a String of quoted values that are comma separated. - def make_header_value(types) - types.map { |t| %("#{t}") }.join(", ") - end + # Public: Transform a clear-site-data config (an Array of Strings) into a + # String that can be used as the value for the clear-site-data header. + # + # types - An Array of String of types of data to clear. + # + # Returns a String of quoted values that are comma separated. + def self.make_header_value(types) + types.map { |t| %("#{t}") }.join(", ") end end end diff --git a/lib/secure_headers/headers/cookie.rb b/lib/secure_headers/headers/cookie.rb index 9d3f7cd1..8f78612c 100644 --- a/lib/secure_headers/headers/cookie.rb +++ b/lib/secure_headers/headers/cookie.rb @@ -7,10 +7,8 @@ module SecureHeaders class CookiesConfigError < StandardError; end class Cookie - class << self - def validate_config!(config) - CookiesConfig.new(config).validate! - end + def self.validate_config!(config) + CookiesConfig.new(config).validate! end attr_reader :raw_cookie, :config diff --git a/lib/secure_headers/headers/expect_certificate_transparency.rb b/lib/secure_headers/headers/expect_certificate_transparency.rb index 4b7272dd..582e8a05 100644 --- a/lib/secure_headers/headers/expect_certificate_transparency.rb +++ b/lib/secure_headers/headers/expect_certificate_transparency.rb @@ -9,31 +9,29 @@ class ExpectCertificateTransparency REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze - class << self - # Public: Generate a expect-ct header. - # - # Returns nil if not configured, returns header name and value if - # configured. - def make_header(config, use_agent = nil) - return if config.nil? || config == OPT_OUT + # Public: Generate a expect-ct header. + # + # Returns nil if not configured, returns header name and value if + # configured. + def self.make_header(config, use_agent = nil) + return if config.nil? || config == OPT_OUT - header = new(config) - [HEADER_NAME, header.value] - end + header = new(config) + [HEADER_NAME, header.value] + end - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash + def self.validate_config!(config) + return if config.nil? || config == OPT_OUT + raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash - unless [true, false, nil].include?(config[:enforce]) - raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR) - end + unless [true, false, nil].include?(config[:enforce]) + raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR) + end - if !config[:max_age] - raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR) - elsif config[:max_age].to_s !~ /\A\d+\z/ - raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR) - end + if !config[:max_age] + raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR) + elsif config[:max_age].to_s !~ /\A\d+\z/ + raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR) end end diff --git a/lib/secure_headers/headers/referrer_policy.rb b/lib/secure_headers/headers/referrer_policy.rb index a4589117..2094656c 100644 --- a/lib/secure_headers/headers/referrer_policy.rb +++ b/lib/secure_headers/headers/referrer_policy.rb @@ -15,29 +15,27 @@ class ReferrerPolicy unsafe-url ) - class << self - # Public: generate an Referrer Policy header. - # - # Returns a default header if no configuration is provided, or a - # header name and value based on the config. - def make_header(config = nil, user_agent = nil) - return if config == OPT_OUT - config ||= DEFAULT_VALUE - [HEADER_NAME, Array(config).join(", ")] - end + # Public: generate an Referrer Policy header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def self.make_header(config = nil, user_agent = nil) + return if config == OPT_OUT + config ||= DEFAULT_VALUE + [HEADER_NAME, Array(config).join(", ")] + end - def validate_config!(config) - case config - when nil, OPT_OUT - # valid - when String, Array - config = Array(config) - unless config.all? { |t| t.is_a?(String) && VALID_POLICIES.include?(t.downcase) } - raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(", ")}") - end - else - raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}") + def self.validate_config!(config) + case config + when nil, OPT_OUT + # valid + when String, Array + config = Array(config) + unless config.all? { |t| t.is_a?(String) && VALID_POLICIES.include?(t.downcase) } + raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(", ")}") end + else + raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}") end end end diff --git a/lib/secure_headers/headers/strict_transport_security.rb b/lib/secure_headers/headers/strict_transport_security.rb index 3d78a484..eaf46cb6 100644 --- a/lib/secure_headers/headers/strict_transport_security.rb +++ b/lib/secure_headers/headers/strict_transport_security.rb @@ -9,21 +9,19 @@ class StrictTransportSecurity VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}" - class << self - # Public: generate an hsts header name, value pair. - # - # Returns a default header if no configuration is provided, or a - # header name and value based on the config. - def make_header(config = nil, user_agent = nil) - return if config == OPT_OUT - [HEADER_NAME, config || DEFAULT_VALUE] - end + # Public: generate an hsts header name, value pair. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def self.make_header(config = nil, user_agent = nil) + return if config == OPT_OUT + [HEADER_NAME, config || DEFAULT_VALUE] + end - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise TypeError.new("Must be a string. Found #{config.class}: #{config} #{config.class}") unless config.is_a?(String) - raise STSConfigError.new(MESSAGE) unless config =~ VALID_STS_HEADER - end + def self.validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config} #{config.class}") unless config.is_a?(String) + raise STSConfigError.new(MESSAGE) unless config =~ VALID_STS_HEADER end end end diff --git a/lib/secure_headers/headers/x_content_type_options.rb b/lib/secure_headers/headers/x_content_type_options.rb index 96f8d314..773bad82 100644 --- a/lib/secure_headers/headers/x_content_type_options.rb +++ b/lib/secure_headers/headers/x_content_type_options.rb @@ -6,22 +6,20 @@ class XContentTypeOptions HEADER_NAME = "x-content-type-options".freeze DEFAULT_VALUE = "nosniff" - class << self - # Public: generate an X-Content-Type-Options header. - # - # Returns a default header if no configuration is provided, or a - # header name and value based on the config. - def make_header(config = nil, user_agent = nil) - return if config == OPT_OUT - [HEADER_NAME, config || DEFAULT_VALUE] - end + # Public: generate an X-Content-Type-Options header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def self.make_header(config = nil, user_agent = nil) + return if config == OPT_OUT + [HEADER_NAME, config || DEFAULT_VALUE] + end - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) - unless config.casecmp(DEFAULT_VALUE) == 0 - raise XContentTypeOptionsConfigError.new("Value can only be nil or 'nosniff'") - end + def self.validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless config.casecmp(DEFAULT_VALUE) == 0 + raise XContentTypeOptionsConfigError.new("Value can only be nil or 'nosniff'") end end end diff --git a/lib/secure_headers/headers/x_download_options.rb b/lib/secure_headers/headers/x_download_options.rb index 1eb1356a..0042e9d0 100644 --- a/lib/secure_headers/headers/x_download_options.rb +++ b/lib/secure_headers/headers/x_download_options.rb @@ -5,22 +5,20 @@ class XDownloadOptions HEADER_NAME = "x-download-options".freeze DEFAULT_VALUE = "noopen" - class << self - # Public: generate an x-download-options header. - # - # Returns a default header if no configuration is provided, or a - # header name and value based on the config. - def make_header(config = nil, user_agent = nil) - return if config == OPT_OUT - [HEADER_NAME, config || DEFAULT_VALUE] - end + # Public: generate an x-download-options header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def self.make_header(config = nil, user_agent = nil) + return if config == OPT_OUT + [HEADER_NAME, config || DEFAULT_VALUE] + end - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) - unless config.casecmp(DEFAULT_VALUE) == 0 - raise XDOConfigError.new("Value can only be nil or 'noopen'") - end + def self.validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless config.casecmp(DEFAULT_VALUE) == 0 + raise XDOConfigError.new("Value can only be nil or 'noopen'") end end end diff --git a/lib/secure_headers/headers/x_frame_options.rb b/lib/secure_headers/headers/x_frame_options.rb index 636e3bfa..9788ca3e 100644 --- a/lib/secure_headers/headers/x_frame_options.rb +++ b/lib/secure_headers/headers/x_frame_options.rb @@ -10,22 +10,20 @@ class XFrameOptions DEFAULT_VALUE = SAMEORIGIN VALID_XFO_HEADER = /\A(#{SAMEORIGIN}\z|#{DENY}\z|#{ALLOW_ALL}\z|#{ALLOW_FROM}[:\s])/i - class << self - # Public: generate an X-Frame-Options header. - # - # Returns a default header if no configuration is provided, or a - # header name and value based on the config. - def make_header(config = nil, user_agent = nil) - return if config == OPT_OUT - [HEADER_NAME, config || DEFAULT_VALUE] - end + # Public: generate an X-Frame-Options header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def self.make_header(config = nil, user_agent = nil) + return if config == OPT_OUT + [HEADER_NAME, config || DEFAULT_VALUE] + end - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) - unless config =~ VALID_XFO_HEADER - raise XFOConfigError.new("Value must be SAMEORIGIN|DENY|ALLOW-FROM:|ALLOWALL") - end + def self.validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless config =~ VALID_XFO_HEADER + raise XFOConfigError.new("Value must be SAMEORIGIN|DENY|ALLOW-FROM:|ALLOWALL") end end end diff --git a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb index a34dd58f..4ca81b68 100644 --- a/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +++ b/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb @@ -6,22 +6,20 @@ class XPermittedCrossDomainPolicies DEFAULT_VALUE = "none" VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) - class << self - # Public: generate an x-permitted-cross-domain-policies header. - # - # Returns a default header if no configuration is provided, or a - # header name and value based on the config. - def make_header(config = nil, user_agent = nil) - return if config == OPT_OUT - [HEADER_NAME, config || DEFAULT_VALUE] - end + # Public: generate an x-permitted-cross-domain-policies header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def self.make_header(config = nil, user_agent = nil) + return if config == OPT_OUT + [HEADER_NAME, config || DEFAULT_VALUE] + end - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) - unless VALID_POLICIES.include?(config.downcase) - raise XPCDPConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") - end + def self.validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + unless VALID_POLICIES.include?(config.downcase) + raise XPCDPConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") end end end diff --git a/lib/secure_headers/headers/x_xss_protection.rb b/lib/secure_headers/headers/x_xss_protection.rb index bd5b7faa..756bc4c3 100644 --- a/lib/secure_headers/headers/x_xss_protection.rb +++ b/lib/secure_headers/headers/x_xss_protection.rb @@ -6,21 +6,19 @@ class XXssProtection DEFAULT_VALUE = "0".freeze VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/ - class << self - # Public: generate an X-Xss-Protection header. - # - # Returns a default header if no configuration is provided, or a - # header name and value based on the config. - def make_header(config = nil, user_agent = nil) - return if config == OPT_OUT - [HEADER_NAME, config || DEFAULT_VALUE] - end + # Public: generate an X-Xss-Protection header. + # + # Returns a default header if no configuration is provided, or a + # header name and value based on the config. + def self.make_header(config = nil, user_agent = nil) + return if config == OPT_OUT + [HEADER_NAME, config || DEFAULT_VALUE] + end - def validate_config!(config) - return if config.nil? || config == OPT_OUT - raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) - raise XXssProtectionConfigError.new("Invalid format (see VALID_X_XSS_HEADER)") unless config.to_s =~ VALID_X_XSS_HEADER - end + def self.validate_config!(config) + return if config.nil? || config == OPT_OUT + raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) + raise XXssProtectionConfigError.new("Invalid format (see VALID_X_XSS_HEADER)") unless config.to_s =~ VALID_X_XSS_HEADER end end end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index ea3e10e9..1c613658 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -99,7 +99,7 @@ module SecureHeaders end it "gives cookies a default config" do - expect(Configuration.default.cookies).to eq({httponly: true, secure: true, samesite: {lax: true}}) + expect(Configuration.default.cookies).to eq({ httponly: true, secure: true, samesite: { lax: true } }) end it "allows OPT_OUT" do @@ -113,11 +113,11 @@ module SecureHeaders it "allows me to be explicit too" do Configuration.default do |config| - config.cookies = {httponly: true, secure: true, samesite: {lax: false}} + config.cookies = { httponly: true, secure: true, samesite: { lax: false } } end config = Configuration.dup - expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}}) + expect(config.cookies).to eq({ httponly: true, secure: true, samesite: { lax: false } }) end end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index 37cb62a7..c16e70a2 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -3,7 +3,7 @@ module SecureHeaders describe ContentSecurityPolicy do - let (:default_opts) do + let(:default_opts) do { default_src: %w(https:), img_src: %w(https: data:), @@ -167,47 +167,47 @@ module SecureHeaders end it "supports strict-dynamic" do - csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}) + csp = ContentSecurityPolicy.new({ default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456 }) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'") end it "supports strict-dynamic and opting out of the appended 'unsafe-inline'" do - csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456, disable_nonce_backwards_compatibility: true }) + csp = ContentSecurityPolicy.new({ default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456, disable_nonce_backwards_compatibility: true }) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'") end it "supports script-src-elem directive" do - csp = ContentSecurityPolicy.new({script_src: %w('self'), script_src_elem: %w('self')}) + csp = ContentSecurityPolicy.new({ script_src: %w('self'), script_src_elem: %w('self') }) expect(csp.value).to eq("script-src 'self'; script-src-elem 'self'") end it "supports script-src-attr directive" do - csp = ContentSecurityPolicy.new({script_src: %w('self'), script_src_attr: %w('self')}) + csp = ContentSecurityPolicy.new({ script_src: %w('self'), script_src_attr: %w('self') }) expect(csp.value).to eq("script-src 'self'; script-src-attr 'self'") end it "supports style-src-elem directive" do - csp = ContentSecurityPolicy.new({style_src: %w('self'), style_src_elem: %w('self')}) + csp = ContentSecurityPolicy.new({ style_src: %w('self'), style_src_elem: %w('self') }) expect(csp.value).to eq("style-src 'self'; style-src-elem 'self'") end it "supports style-src-attr directive" do - csp = ContentSecurityPolicy.new({style_src: %w('self'), style_src_attr: %w('self')}) + csp = ContentSecurityPolicy.new({ style_src: %w('self'), style_src_attr: %w('self') }) expect(csp.value).to eq("style-src 'self'; style-src-attr 'self'") end it "supports trusted-types directive" do - csp = ContentSecurityPolicy.new({trusted_types: %w(blahblahpolicy)}) + csp = ContentSecurityPolicy.new({ trusted_types: %w(blahblahpolicy) }) expect(csp.value).to eq("trusted-types blahblahpolicy") end it "supports trusted-types directive with 'none'" do - csp = ContentSecurityPolicy.new({trusted_types: %w('none')}) + csp = ContentSecurityPolicy.new({ trusted_types: %w('none') }) expect(csp.value).to eq("trusted-types 'none'") end it "allows duplicate policy names in trusted-types directive" do - csp = ContentSecurityPolicy.new({trusted_types: %w(blahblahpolicy 'allow-duplicates')}) + csp = ContentSecurityPolicy.new({ trusted_types: %w(blahblahpolicy 'allow-duplicates') }) expect(csp.value).to eq("trusted-types blahblahpolicy 'allow-duplicates'") end end diff --git a/spec/lib/secure_headers/headers/cookie_spec.rb b/spec/lib/secure_headers/headers/cookie_spec.rb index cdb24246..b0a9152b 100644 --- a/spec/lib/secure_headers/headers/cookie_spec.rb +++ b/spec/lib/secure_headers/headers/cookie_spec.rb @@ -35,7 +35,7 @@ module SecureHeaders context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do - cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}, httponly: OPT_OUT, samesite: OPT_OUT) + cookie = Cookie.new(raw_cookie, secure: { only: ["_session"] }, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end @@ -56,7 +56,7 @@ module SecureHeaders context "when configured with a Hash" do it "flags cookies as HttpOnly when whitelisted" do - cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}, secure: OPT_OUT, samesite: OPT_OUT) + cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"] }, secure: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end @@ -75,7 +75,7 @@ module SecureHeaders end it "flags SameSite=#{flag} when configured with a boolean" do - cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => true}, secure: OPT_OUT, httponly: OPT_OUT) + cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => true }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}") end @@ -86,7 +86,7 @@ module SecureHeaders end it "flags SameSite=Strict when configured with a boolean" do - cookie = Cookie.new(raw_cookie, {samesite: { strict: true}, secure: OPT_OUT, httponly: OPT_OUT}) + cookie = Cookie.new(raw_cookie, { samesite: { strict: true }, secure: OPT_OUT, httponly: OPT_OUT }) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end @@ -146,7 +146,7 @@ module SecureHeaders (cookie_options - [flag]).each do |other_flag| it "raises an exception when SameSite #{flag} and #{other_flag} enforcement modes are configured with booleans" do expect do - Cookie.validate_config!(samesite: { flag => true, other_flag => true}) + Cookie.validate_config!(samesite: { flag => true, other_flag => true }) end.to raise_error(CookiesConfigError) end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index c621e88e..99065744 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -8,7 +8,7 @@ module SecureHeaders Configuration.default end - let (:default_opts) do + let(:default_opts) do { default_src: %w(https:), img_src: %w(https: data:), diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index b3925f85..f019b597 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -42,7 +42,7 @@ module SecureHeaders end context "cookies should be flagged" do it "flags cookies as secure" do - Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} } + Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar; secure") @@ -62,7 +62,7 @@ module SecureHeaders context "cookies should not be flagged" do it "does not flags cookies as secure" do - Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT, samesite: OPT_OUT} } + Configuration.default { |config| config.cookies = { secure: OPT_OUT, httponly: OPT_OUT, samesite: OPT_OUT } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") @@ -75,7 +75,7 @@ module SecureHeaders reset_config end it "flags cookies from configuration" do - Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } } + Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true } } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env @@ -85,7 +85,7 @@ module SecureHeaders it "flags cookies with a combination of SameSite configurations" do cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] }) - Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT} } + Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index fd66d487..76d51baf 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -54,7 +54,7 @@ module SecureHeaders describe "#header_hash_for" do it "allows you to opt out of individual headers via API" do Configuration.default do |config| - config.csp = { default_src: %w('self'), script_src: %w('self')} + config.csp = { default_src: %w('self'), script_src: %w('self') } config.csp_report_only = config.csp end SecureHeaders.opt_out_of_header(request, :csp) @@ -174,11 +174,11 @@ module SecureHeaders end Configuration.named_append(:moar_default_sources) do |request| - { default_src: %w(https:), style_src: %w('self')} + { default_src: %w(https:), style_src: %w('self') } end Configuration.named_append(:how_about_a_script_src_too) do |request| - { script_src: %w('unsafe-inline')} + { script_src: %w('unsafe-inline') } end SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources) @@ -318,7 +318,7 @@ module SecureHeaders default_src: %w('self'), script_src: %w('self') } - config.csp_report_only = config.csp.merge({script_src: %w(foo.com)}) + config.csp_report_only = config.csp.merge({ script_src: %w(foo.com) }) end hash = SecureHeaders.header_hash_for(request) @@ -342,42 +342,42 @@ module SecureHeaders end it "allows appending to the enforced policy" do - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) + SecureHeaders.append_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self'") end it "allows appending to the report only policy" do - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) + SecureHeaders.append_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }, :report_only) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self'") expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows appending to both policies" do - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) + SecureHeaders.append_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }, :both) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows overriding the enforced policy" do - SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) + SecureHeaders.override_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src anothercdn.com") expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self'") end it "allows overriding the report only policy" do - SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) + SecureHeaders.override_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }, :report_only) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self'") expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src anothercdn.com") end it "allows overriding both policies" do - SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) + SecureHeaders.override_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }, :both) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src anothercdn.com") expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src anothercdn.com") @@ -392,7 +392,7 @@ module SecureHeaders script_src: %w('self') } end - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) + SecureHeaders.append_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") @@ -408,7 +408,7 @@ module SecureHeaders script_src: %w('self') } end - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) + SecureHeaders.append_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy-report-only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") @@ -427,7 +427,7 @@ module SecureHeaders script_src: %w('self') } end - SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) + SecureHeaders.append_content_security_policy_directives(request, { script_src: %w(anothercdn.com) }) hash = SecureHeaders.header_hash_for(request) expect(hash["content-security-policy"]).to eq("default-src enforced.com; script-src 'self' anothercdn.com") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b0c774d9..65627eec 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -41,18 +41,16 @@ def expect_default_values(hash) module SecureHeaders class Configuration - class << self - def clear_default_config - remove_instance_variable(:@default_config) if defined?(@default_config) - end + def self.clear_default_config + remove_instance_variable(:@default_config) if defined?(@default_config) + end - def clear_overrides - remove_instance_variable(:@overrides) if defined?(@overrides) - end + def self.clear_overrides + remove_instance_variable(:@overrides) if defined?(@overrides) + end - def clear_appends - remove_instance_variable(:@appends) if defined?(@appends) - end + def self.clear_appends + remove_instance_variable(:@appends) if defined?(@appends) end end end From 3f2e6cd4a10ebdf45ddf27e4d19fbf549eeeb3f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:04:43 +0000 Subject: [PATCH 623/636] Bump ruby/setup-ruby from 1.269.0 to 1.270.0 Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.269.0 to 1.270.0. - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/d697be2f83c6234b20877c3b5eac7a7f342f0d0c...ac793fdd38cc468a4dd57246fa9d0e868aba9085) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.270.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ee26388..ab607c3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c #v1.269.0 tag + uses: ruby/setup-ruby@ac793fdd38cc468a4dd57246fa9d0e868aba9085 #v1.269.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 727febbaf572291dd80e0d99618d2bff623fe275 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:04:47 +0000 Subject: [PATCH 624/636] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ee26388..24edad6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: ruby: [ '2.7', '3.0', '3.1', '3.2', '3.4', '4.0' ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby }} uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c #v1.269.0 tag with: From 8c2884bfed689144e2da1f338f6a991e1723d648 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:11:14 -0500 Subject: [PATCH 625/636] Bump ruby/setup-ruby from 1.270.0 to 1.275.0 (#572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.270.0 to 1.275.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.275.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.274.0...v1.275.0

v1.274.0

What's Changed

New Contributors

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.273.0...v1.274.0

v1.273.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.272.0...v1.273.0

v1.272.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.271.0...v1.272.0

v1.271.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.270.0...v1.271.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.270.0&new-version=1.275.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f1d4595..cf6ecc55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@ac793fdd38cc468a4dd57246fa9d0e868aba9085 #v1.269.0 tag + uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b #v1.269.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From eac8f6e7667a2c44681fa8ac32337587da2915d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:02:46 +0000 Subject: [PATCH 626/636] Bump ruby/setup-ruby from 1.275.0 to 1.276.0 Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.275.0 to 1.276.0. - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/d354de180d0c9e813cfddfcbdc079945d4be589b...ae195bbe749a7cef685ac729197124a48305c1cb) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.276.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf6ecc55..4057f583 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b #v1.269.0 tag + uses: ruby/setup-ruby@ae195bbe749a7cef685ac729197124a48305c1cb #v1.269.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From d205061b94c91451a6135c43e2df3612c4dd6bd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:19:30 +0000 Subject: [PATCH 627/636] Bump ruby/setup-ruby from 1.276.0 to 1.281.0 Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.276.0 to 1.281.0. - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/ae195bbe749a7cef685ac729197124a48305c1cb...675dd7ba1b06c8786a1480d89c384f5620a42647) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.281.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4057f583..b190aad2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@ae195bbe749a7cef685ac729197124a48305c1cb #v1.269.0 tag + uses: ruby/setup-ruby@675dd7ba1b06c8786a1480d89c384f5620a42647 #v1.269.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From bf0660271aa8fa7ff872990853a3cb86b7695c9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:16:27 -0500 Subject: [PATCH 628/636] Bump ruby/setup-ruby from 1.281.0 to 1.286.0 (#577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.281.0 to 1.286.0.
Release notes

Sourced from ruby/setup-ruby's releases.

v1.286.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.285.0...v1.286.0

v1.285.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.284.0...v1.285.0

v1.284.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.283.0...v1.284.0

v1.283.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.282.0...v1.283.0

v1.282.0

What's Changed

Full Changelog: https://github.com/ruby/setup-ruby/compare/v1.281.0...v1.282.0

Commits
  • 90be115 Add truffleruby-33.0.1,truffleruby+graalvm-33.0.1
  • e69dcf3 Update all dependencies to latest
  • 9f55308 Convert to String earlier in generate-windows-versions.rb
  • 80740b3 Add new RubyInstaller releases 4.0.1-1 and 3.2.10-1
  • 5fcbc91 Fix compatibility to ruby-3.2
  • 708024e Add ruby-3.2.10
  • 757ecf5 Give a proper name to CI jobs checking generated files
  • 6963d48 Use Regexp.escape to not need to manually escape (error-prone)
  • 3fc6249 Match more strictly with \A and \z
  • b939495 Add restriction and validation for download urls
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.281.0&new-version=1.286.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b190aad2..6167e0c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@675dd7ba1b06c8786a1480d89c384f5620a42647 #v1.269.0 tag + uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 #v1.269.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 72fbd0244dea9ea0347f64bc5856293aa6e82e8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:49:12 +0000 Subject: [PATCH 629/636] Bump ruby/setup-ruby from 1.286.0 to 1.287.0 Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.286.0 to 1.287.0. - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/90be1154f987f4dc0fe0dd0feedac9e473aa4ba8...8d27f39a5e7ad39aebbcbd1324f7af020229645c) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.287.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6167e0c5..3ba7ee3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 #v1.269.0 tag + uses: ruby/setup-ruby@8d27f39a5e7ad39aebbcbd1324f7af020229645c #v1.269.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 46e8335d18756cf1d8a15dc6cbc3c793bbc583a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:38:37 +0000 Subject: [PATCH 630/636] Bump ruby/setup-ruby from 1.287.0 to 1.288.0 Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.287.0 to 1.288.0. - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/8d27f39a5e7ad39aebbcbd1324f7af020229645c...09a7688d3b55cf0e976497ff046b70949eeaccfd) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.288.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ba7ee3c..1ea70944 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@8d27f39a5e7ad39aebbcbd1324f7af020229645c #v1.269.0 tag + uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd #v1.269.0 tag with: ruby-version: ${{ matrix.ruby }} bundler-cache: true From 7f83b9315be3261475e2bd3aa7205e4abf3cbcc6 Mon Sep 17 00:00:00 2001 From: Dennis Pacewicz <119963426+rei-moo@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:07:14 -0500 Subject: [PATCH 631/636] 7.2 release (#566) - https://github.com/github/secure_headers/pull/551 - https://github.com/github/secure_headers/pull/555 - https://github.com/github/secure_headers/pull/477 - https://github.com/github/secure_headers/pull/485 - https://github.com/github/secure_headers/pull/568 - https://github.com/github/secure_headers/pull/570 Fixes: #541 Fixes: #514 Fixes: #450 Fixes: #348 --------- Co-authored-by: Dmytro Bihniak Co-authored-by: Aaron Pfeifer Co-authored-by: Keith Cirkel Co-authored-by: Dusty Greif Co-authored-by: Matt Langlois Co-authored-by: Ryan Ahearn Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com>m> Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Co-authored-by: Kylie Stradley <4666485+KyFaSt@users.noreply.github.com> Co-authored-by: Kylie Stradley --- README.md | 64 ++++++ lib/secure_headers.rb | 24 ++ lib/secure_headers/configuration.rb | 57 ++++- .../headers/content_security_policy.rb | 33 ++- .../headers/policy_management.rb | 28 ++- .../headers/reporting_endpoints.rb | 54 +++++ lib/secure_headers/middleware.rb | 18 +- lib/secure_headers/railtie.rb | 9 +- lib/secure_headers/task_helper.rb | 65 ++++++ lib/tasks/tasks.rake | 57 +---- spec/lib/secure_headers/configuration_spec.rb | 89 ++++++++ .../headers/content_security_policy_spec.rb | 37 +++- .../headers/policy_management_spec.rb | 24 ++ .../headers/reporting_endpoints_spec.rb | 125 +++++++++++ spec/lib/secure_headers/middleware_spec.rb | 61 ++++- spec/lib/secure_headers/railtie_spec.rb | 163 ++++++++++++++ spec/lib/secure_headers/task_helper_spec.rb | 123 +++++++++++ spec/lib/secure_headers_spec.rb | 208 ++++++++++++++++++ spec/spec_helper.rb | 6 + 19 files changed, 1168 insertions(+), 77 deletions(-) create mode 100644 lib/secure_headers/headers/reporting_endpoints.rb create mode 100644 lib/secure_headers/task_helper.rb create mode 100644 spec/lib/secure_headers/headers/reporting_endpoints_spec.rb create mode 100644 spec/lib/secure_headers/railtie_spec.rb create mode 100644 spec/lib/secure_headers/task_helper_spec.rb diff --git a/README.md b/README.md index 4682249d..344d1693 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,50 @@ SecureHeaders::Configuration.default do |config| img_src: %w(somewhereelse.com), report_uri: %w(https://report-uri.io/example-csp-report-only) }) + + # Optional: Use the modern report-to directive (with Reporting-Endpoints header) + config.csp = config.csp.merge({ + report_to: "csp-endpoint" + }) + + # When using report-to, configure the reporting endpoints header + config.reporting_endpoints = { + "csp-endpoint": "https://report-uri.io/example-csp", + "csp-report-only": "https://report-uri.io/example-csp-report-only" + } end ``` +### CSP Reporting + +SecureHeaders supports both the legacy `report-uri` and the modern `report-to` directives for CSP violation reporting: + +#### report-uri (Legacy) +The `report-uri` directive sends violations to a URL endpoint. It's widely supported but limited to POST requests with JSON payloads. + +```ruby +config.csp = { + default_src: %w('self'), + report_uri: %w(https://example.com/csp-report) +} +``` + +#### report-to (Modern) +The `report-to` directive specifies a named reporting endpoint defined in the `Reporting-Endpoints` header. This enables more flexible reporting through the HTTP Reporting API standard. + +```ruby +config.csp = { + default_src: %w('self'), + report_to: "csp-endpoint" +} + +config.reporting_endpoints = { + "csp-endpoint": "https://example.com/reports" +} +``` + +**Recommendation:** Use both `report-uri` and `report-to` for maximum compatibility while transitioning to the modern approach. + ### Deprecated Configuration Values * `block_all_mixed_content` - this value is deprecated in favor of `upgrade_insecure_requests`. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/block-all-mixed-content for more information. @@ -125,6 +166,29 @@ end However, I would consider these headers anyways depending on your load and bandwidth requirements. +## Disabling secure_headers + +If you want to disable `secure_headers` entirely (e.g., for specific environments or deployment scenarios), you can use `Configuration.disable!`: + +```ruby +if ENV["ENABLE_STRICT_HEADERS"] + SecureHeaders::Configuration.default do |config| + # your configuration here + end +else + SecureHeaders::Configuration.disable! +end +``` + +**Important**: This configuration must be set during application startup (e.g., in an initializer). Once you call either `Configuration.default` or `Configuration.disable!`, the choice cannot be changed at runtime. Attempting to call `disable!` after `default` (or vice versa) will raise an `AlreadyConfiguredError`. + +When disabled, no security headers will be set by the gem. This is useful when: +- You're gradually rolling out secure_headers across different customers or deployments +- You need to migrate existing custom headers to secure_headers +- You want environment-specific control over security headers + +Note: When `disable!` is used, you don't need to configure a default configuration. The gem will not raise a `NotYetConfiguredError`. + ## Acknowledgements This project originated within the Security team at Twitter. An archived fork from the point of transition is here: https://github.com/twitter-archive/secure_headers. diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index ed4efd93..4caf1044 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -11,6 +11,7 @@ require "secure_headers/headers/referrer_policy" require "secure_headers/headers/clear_site_data" require "secure_headers/headers/expect_certificate_transparency" +require "secure_headers/headers/reporting_endpoints" require "secure_headers/middleware" require "secure_headers/railtie" require "secure_headers/view_helper" @@ -133,6 +134,7 @@ def opt_out_of_all_protection(request) # request. # # StrictTransportSecurity is not applied to http requests. + # upgrade_insecure_requests is not applied to http requests. # See #config_for to determine which config is used for a given request. # # Returns a hash of header names => header values. The value @@ -146,6 +148,11 @@ def header_hash_for(request) if request.scheme != HTTPS headers.delete(StrictTransportSecurity::HEADER_NAME) + + # Remove upgrade_insecure_requests from CSP headers for HTTP requests + # as it doesn't make sense to upgrade requests when the page itself is served over HTTP + remove_upgrade_insecure_requests_from_csp!(headers, config.csp) + remove_upgrade_insecure_requests_from_csp!(headers, config.csp_report_only) end headers end @@ -242,6 +249,23 @@ def content_security_policy_nonce(request, script_or_style) def override_secure_headers_request_config(request, config) request.env[SECURE_HEADERS_CONFIG] = config end + + # Private: removes upgrade_insecure_requests directive from a CSP config + # if it's present, and updates the headers hash with the modified CSP. + # + # headers - the headers hash to update + # csp_config - the CSP config to check and potentially modify + # + # Returns nothing (modifies headers in place) + def remove_upgrade_insecure_requests_from_csp!(headers, csp_config) + return if csp_config.opt_out? + return unless csp_config.directive_value(ContentSecurityPolicy::UPGRADE_INSECURE_REQUESTS) + + modified_config = csp_config.dup + modified_config.update_directive(ContentSecurityPolicy::UPGRADE_INSECURE_REQUESTS, false) + header_name, value = ContentSecurityPolicy.make_header(modified_config) + headers[header_name] = value if header_name && value + end end # These methods are mixed into controllers and delegate to the class method diff --git a/lib/secure_headers/configuration.rb b/lib/secure_headers/configuration.rb index 4fd459ea..2ef02d5d 100644 --- a/lib/secure_headers/configuration.rb +++ b/lib/secure_headers/configuration.rb @@ -9,23 +9,53 @@ class AlreadyConfiguredError < StandardError; end class NotYetConfiguredError < StandardError; end class IllegalPolicyModificationError < StandardError; end class << self + # Public: Disable secure_headers entirely. When disabled, no headers will be set. + # + # Note: This must be called before Configuration.default. Calling it after + # Configuration.default has been set will raise an AlreadyConfiguredError. + # + # Returns nothing + # Raises AlreadyConfiguredError if Configuration.default has already been called + def disable! + if defined?(@default_config) + raise AlreadyConfiguredError, "Configuration already set, cannot disable" + end + + @disabled = true + @noop_config = create_noop_config.freeze + + # Ensure the built-in NOOP override is available even if `default` has never been called + @overrides ||= {} + unless @overrides.key?(NOOP_OVERRIDE) + @overrides[NOOP_OVERRIDE] = method(:create_noop_config_block) + end + end + + # Public: Check if secure_headers is disabled + # + # Returns boolean + def disabled? + defined?(@disabled) && @disabled + end + # Public: Set the global default configuration. # # Optionally supply a block to override the defaults set by this library. # # Returns the newly created config. + # Raises AlreadyConfiguredError if Configuration.disable! has already been called def default(&block) + if disabled? + raise AlreadyConfiguredError, "Configuration has been disabled, cannot set default" + end + if defined?(@default_config) raise AlreadyConfiguredError, "Policy already configured" end # Define a built-in override that clears all configuration options and # results in no security headers being set. - override(NOOP_OVERRIDE) do |config| - CONFIG_ATTRIBUTES.each do |attr| - config.instance_variable_set("@#{attr}", OPT_OUT) - end - end + override(NOOP_OVERRIDE, &method(:create_noop_config_block)) new_config = new(&block).freeze new_config.validate_config! @@ -101,6 +131,7 @@ def deep_copy(config) # of ensuring that the default config is never mutated and is dup(ed) # before it is used in a request. def default_config + return @noop_config if disabled? unless defined?(@default_config) raise NotYetConfiguredError, "Default policy not yet configured" end @@ -116,6 +147,19 @@ def deep_copy_if_hash(value) value end end + + # Private: Creates a NOOP configuration that opts out of all headers + def create_noop_config + new(&method(:create_noop_config_block)) + end + + # Private: Block for creating NOOP configuration + # Used by both create_noop_config and the NOOP_OVERRIDE mechanism + def create_noop_config_block(config) + CONFIG_ATTRIBUTES.each do |attr| + config.instance_variable_set("@#{attr}", OPT_OUT) + end + end end CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = { @@ -131,6 +175,7 @@ def deep_copy_if_hash(value) csp: ContentSecurityPolicy, csp_report_only: ContentSecurityPolicy, cookies: Cookie, + reporting_endpoints: ReportingEndpoints, }.freeze CONFIG_ATTRIBUTES = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys.freeze @@ -167,6 +212,7 @@ def initialize(&block) @x_permitted_cross_domain_policies = nil @x_xss_protection = nil @expect_certificate_transparency = nil + @reporting_endpoints = nil self.referrer_policy = OPT_OUT self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT) @@ -192,6 +238,7 @@ def dup copy.clear_site_data = @clear_site_data copy.expect_certificate_transparency = @expect_certificate_transparency copy.referrer_policy = @referrer_policy + copy.reporting_endpoints = self.class.send(:deep_copy_if_hash, @reporting_endpoints) copy end diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 055771f0..e93eca21 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -63,6 +63,8 @@ def build_value build_sandbox_list_directive(directive_name) when :media_type_list build_media_type_list_directive(directive_name) + when :report_to_endpoint + build_report_to_directive(directive_name) end end.compact.join("; ") end @@ -100,6 +102,13 @@ def build_media_type_list_directive(directive) end end + def build_report_to_directive(directive) + return unless endpoint_name = @config.directive_value(directive) + if endpoint_name && endpoint_name.is_a?(String) && !endpoint_name.empty? + [symbol_to_hyphen_case(directive), endpoint_name].join(" ") + end + end + # Private: builds a string that represents one directive in a minified form. # # directive_name - a symbol representing the various ALL_DIRECTIVES @@ -129,6 +138,7 @@ def minify_source_list(directive, source_list) else source_list = populate_nonces(directive, source_list) source_list = reject_all_values_if_none(source_list) + source_list = normalize_uri_paths(source_list) unless directive == REPORT_URI || @preserve_schemes source_list = strip_source_schemes(source_list) @@ -151,6 +161,26 @@ def reject_all_values_if_none(source_list) end end + def normalize_uri_paths(source_list) + source_list.map do |source| + # Normalize domains ending in a single / as without omitting the slash accomplishes the same. + # https://www.w3.org/TR/CSP3/#match-paths § 6.6.2.10 Step 2 + begin + uri = URI(source) + if uri.path == "/" + next source.chomp("/") + end + rescue URI::InvalidURIError + end + + if source.chomp("/").include?("/") + source + else + source.chomp("/") + end + end + end + # Private: append a nonce to the script/style directories if script_nonce # or style_nonce are provided. def populate_nonces(directive, source_list) @@ -179,11 +209,12 @@ def append_nonce(source_list, nonce) end # Private: return the list of directives, - # starting with default-src and ending with report-uri. + # starting with default-src and ending with reporting directives (alphabetically ordered). def directives [ DEFAULT_SRC, BODY_DIRECTIVES, + REPORT_TO, REPORT_URI, ].flatten end diff --git a/lib/secure_headers/headers/policy_management.rb b/lib/secure_headers/headers/policy_management.rb index d34c8a8c..de5958e3 100644 --- a/lib/secure_headers/headers/policy_management.rb +++ b/lib/secure_headers/headers/policy_management.rb @@ -39,6 +39,7 @@ def self.included(base) SCRIPT_SRC = :script_src STYLE_SRC = :style_src REPORT_URI = :report_uri + REPORT_TO = :report_to DIRECTIVES_1_0 = [ DEFAULT_SRC, @@ -51,7 +52,8 @@ def self.included(base) SANDBOX, SCRIPT_SRC, STYLE_SRC, - REPORT_URI + REPORT_URI, + REPORT_TO ].freeze BASE_URI = :base_uri @@ -110,9 +112,9 @@ def self.included(base) ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_EXPERIMENTAL).uniq.sort - # Think of default-src and report-uri as the beginning and end respectively, + # Think of default-src and report-uri/report-to as the beginning and end respectively, # everything else is in between. - BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] + BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI, REPORT_TO] DIRECTIVE_VALUE_TYPES = { BASE_URI => :source_list, @@ -129,10 +131,11 @@ def self.included(base) NAVIGATE_TO => :source_list, OBJECT_SRC => :source_list, PLUGIN_TYPES => :media_type_list, + PREFETCH_SRC => :source_list, + REPORT_TO => :report_to_endpoint, + REPORT_URI => :source_list, REQUIRE_SRI_FOR => :require_sri_for_list, REQUIRE_TRUSTED_TYPES_FOR => :require_trusted_types_for_list, - REPORT_URI => :source_list, - PREFETCH_SRC => :source_list, SANDBOX => :sandbox_list, SCRIPT_SRC => :source_list, SCRIPT_SRC_ELEM => :source_list, @@ -158,6 +161,7 @@ def self.included(base) FORM_ACTION, FRAME_ANCESTORS, NAVIGATE_TO, + REPORT_TO, REPORT_URI, ] @@ -344,6 +348,8 @@ def validate_directive!(directive, value) validate_require_sri_source_expression!(directive, value) when :require_trusted_types_for_list validate_require_trusted_types_for_source_expression!(directive, value) + when :report_to_endpoint + validate_report_to_endpoint_expression!(directive, value) else raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") end @@ -398,6 +404,18 @@ def validate_require_trusted_types_for_source_expression!(directive, require_tru end end + # Private: validates that a report-to endpoint expression: + # 1. is a string + # 2. is not empty + def validate_report_to_endpoint_expression!(directive, endpoint_name) + unless endpoint_name.is_a?(String) + raise ContentSecurityPolicyConfigError.new("#{directive} must be a string. Found #{endpoint_name.class} value") + end + if endpoint_name.empty? + raise ContentSecurityPolicyConfigError.new("#{directive} must not be empty") + end + end + # Private: validates that a source expression: # 1. is an array of strings # 2. does not contain any deprecated, now invalid values (inline, eval, self, none) diff --git a/lib/secure_headers/headers/reporting_endpoints.rb b/lib/secure_headers/headers/reporting_endpoints.rb new file mode 100644 index 00000000..0a3ff12a --- /dev/null +++ b/lib/secure_headers/headers/reporting_endpoints.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +module SecureHeaders + class ReportingEndpointsConfigError < StandardError; end + class ReportingEndpoints + HEADER_NAME = "reporting-endpoints".freeze + + class << self + # Public: generate a Reporting-Endpoints header. + # + # The config should be a Hash of endpoint names to URLs. + # Example: { "csp-endpoint" => "https://example.com/reports" } + # + # Returns nil if config is OPT_OUT or nil, or a header name and + # formatted header value based on the config. + def make_header(config = nil) + return if config.nil? || config == OPT_OUT + validate_config!(config) + [HEADER_NAME, format_endpoints(config)] + end + + def validate_config!(config) + case config + when nil, OPT_OUT + # valid + when Hash + config.each_pair do |name, url| + if name.is_a?(Symbol) + name = name.to_s + end + unless name.is_a?(String) && !name.empty? + raise ReportingEndpointsConfigError.new("Endpoint name must be a non-empty string, got: #{name.inspect}") + end + unless url.is_a?(String) && !url.empty? + raise ReportingEndpointsConfigError.new("Endpoint URL must be a non-empty string, got: #{url.inspect}") + end + unless url.start_with?("https://") + raise ReportingEndpointsConfigError.new("Endpoint URLs must use https, got: #{url.inspect}") + end + end + else + raise TypeError.new("Must be a Hash of endpoint names to URLs. Found #{config.class}: #{config}") + end + end + + private + + def format_endpoints(config) + config.map do |name, url| + %{#{name}="#{url}"} + end.join(", ") + end + end + end +end diff --git a/lib/secure_headers/middleware.rb b/lib/secure_headers/middleware.rb index e1c07c5b..48f1e772 100644 --- a/lib/secure_headers/middleware.rb +++ b/lib/secure_headers/middleware.rb @@ -10,6 +10,12 @@ def call(env) req = Rack::Request.new(env) status, headers, response = @app.call(env) + # Rack::Headers is available in Rack 3.x and later + # So we should pull the headers into that structure if possible + if defined?(Rack::Headers) + headers = Rack::Headers[headers] + end + config = SecureHeaders.config_for(req) flag_cookies!(headers, override_secure(env, config.cookies)) unless config.cookies == OPT_OUT headers.merge!(SecureHeaders.header_hash_for(req)) @@ -20,14 +26,12 @@ def call(env) # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194 def flag_cookies!(headers, config) - if cookies = headers["Set-Cookie"] - # Support Rails 2.3 / Rack 1.1 arrays as headers - cookies = cookies.split("\n") unless cookies.is_a?(Array) + cookies = headers["Set-Cookie"] + return unless cookies - headers["Set-Cookie"] = cookies.map do |cookie| - SecureHeaders::Cookie.new(cookie, config).to_s - end.join("\n") - end + cookies_array = cookies.is_a?(Array) ? cookies : cookies.split("\n") + secured_cookies = cookies_array.map { |cookie| SecureHeaders::Cookie.new(cookie, config).to_s } + headers["Set-Cookie"] = cookies.is_a?(Array) ? secured_cookies : secured_cookies.join("\n") end # disable Secure cookies for non-https requests diff --git a/lib/secure_headers/railtie.rb b/lib/secure_headers/railtie.rb index ba255acc..64f9eec9 100644 --- a/lib/secure_headers/railtie.rb +++ b/lib/secure_headers/railtie.rb @@ -22,9 +22,12 @@ class Railtie < Rails::Railtie ActiveSupport.on_load(:action_controller) do include SecureHeaders - unless Rails.application.config.action_dispatch.default_headers.nil? - conflicting_headers.each do |header| - Rails.application.config.action_dispatch.default_headers.delete(header) + default_headers = Rails.application.config.action_dispatch.default_headers + unless default_headers.nil? + default_headers.each_key do |header| + if conflicting_headers.include?(header.downcase) + default_headers.delete(header) + end end end end diff --git a/lib/secure_headers/task_helper.rb b/lib/secure_headers/task_helper.rb new file mode 100644 index 00000000..fa4ba971 --- /dev/null +++ b/lib/secure_headers/task_helper.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module SecureHeaders + module TaskHelper + include SecureHeaders::HashHelper + + INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx + INLINE_STYLE_REGEX = /(]*>)(.*?)<\/style>/mx + INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx + INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx + + def generate_inline_script_hashes(filename) + hashes = [] + + hashes.concat find_inline_content(filename, INLINE_SCRIPT_REGEX, false) + hashes.concat find_inline_content(filename, INLINE_HASH_SCRIPT_HELPER_REGEX, true) + + hashes + end + + def generate_inline_style_hashes(filename) + hashes = [] + + hashes.concat find_inline_content(filename, INLINE_STYLE_REGEX, false) + hashes.concat find_inline_content(filename, INLINE_HASH_STYLE_HELPER_REGEX, true) + + hashes + end + + def dynamic_content?(filename, inline_script) + !!( + (is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) || + (is_erb?(filename) && inline_script =~ /<%.*%>/) + ) + end + + private + + def find_inline_content(filename, regex, strip_trailing_whitespace) + hashes = [] + file = File.read(filename) + file.scan(regex) do # TODO don't use gsub + inline_script = Regexp.last_match.captures.last + inline_script.gsub!(/(\r?\n)[\t ]+\z/, '\1') if strip_trailing_whitespace + if dynamic_content?(filename, inline_script) + puts "Looks like there's some dynamic content inside of a tag :-/" + puts "That pretty much means the hash value will never match." + puts "Code: " + inline_script + puts "=" * 20 + end + + hashes << hash_source(inline_script) + end + hashes + end + + def is_erb?(filename) + filename =~ /\.erb\Z/ + end + + def is_mustache?(filename) + filename =~ /\.mustache\Z/ + end + end +end diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake index cb078246..09237bb4 100644 --- a/lib/tasks/tasks.rake +++ b/lib/tasks/tasks.rake @@ -1,58 +1,8 @@ # frozen_string_literal: true -INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX -INLINE_STYLE_REGEX = /(]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX -INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX -INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX +require "secure_headers/task_helper" namespace :secure_headers do - include SecureHeaders::HashHelper - - def is_erb?(filename) - filename =~ /\.erb\Z/ - end - - def is_mustache?(filename) - filename =~ /\.mustache\Z/ - end - - def dynamic_content?(filename, inline_script) - (is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) || - (is_erb?(filename) && inline_script =~ /<%.*%>/) - end - - def find_inline_content(filename, regex, hashes, strip_trailing_whitespace) - file = File.read(filename) - file.scan(regex) do # TODO don't use gsub - inline_script = Regexp.last_match.captures.last - inline_script.gsub!(/(\r?\n)[\t ]+\z/, '\1') if strip_trailing_whitespace - if dynamic_content?(filename, inline_script) - puts "Looks like there's some dynamic content inside of a tag :-/" - puts "That pretty much means the hash value will never match." - puts "Code: " + inline_script - puts "=" * 20 - end - - hashes << hash_source(inline_script) - end - end - - def generate_inline_script_hashes(filename) - hashes = [] - - find_inline_content(filename, INLINE_SCRIPT_REGEX, hashes, false) - find_inline_content(filename, INLINE_HASH_SCRIPT_HELPER_REGEX, hashes, true) - - hashes - end - - def generate_inline_style_hashes(filename) - hashes = [] - - find_inline_content(filename, INLINE_STYLE_REGEX, hashes, false) - find_inline_content(filename, INLINE_HASH_STYLE_HELPER_REGEX, hashes, true) - - hashes - end + include SecureHeaders::TaskHelper desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" task :generate_hashes do |t, args| @@ -77,6 +27,7 @@ namespace :secure_headers do file.write(script_hashes.to_yaml) end - puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" + file_count = (script_hashes["scripts"].keys + script_hashes["styles"].keys).uniq.count + puts "Script and style hashes from #{file_count} files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" end end diff --git a/spec/lib/secure_headers/configuration_spec.rb b/spec/lib/secure_headers/configuration_spec.rb index 1c613658..420e4f26 100644 --- a/spec/lib/secure_headers/configuration_spec.rb +++ b/spec/lib/secure_headers/configuration_spec.rb @@ -119,5 +119,94 @@ module SecureHeaders config = Configuration.dup expect(config.cookies).to eq({ httponly: true, secure: true, samesite: { lax: false } }) end + + describe ".disable!" do + it "disables secure_headers completely" do + Configuration.disable! + expect(Configuration.disabled?).to be true + end + + it "returns a noop config when disabled" do + Configuration.disable! + config = Configuration.send(:default_config) + Configuration::CONFIG_ATTRIBUTES.each do |attr| + expect(config.instance_variable_get("@#{attr}")).to eq(OPT_OUT) + end + end + + it "does not raise NotYetConfiguredError when disabled without default config" do + Configuration.disable! + expect { Configuration.send(:default_config) }.not_to raise_error + end + + it "registers the NOOP_OVERRIDE when disabled without calling default" do + Configuration.disable! + expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil + end + + it "raises AlreadyConfiguredError when called after default" do + Configuration.default do |config| + config.csp = { default_src: %w('self'), script_src: %w('self') } + end + + expect { + Configuration.disable! + }.to raise_error(Configuration::AlreadyConfiguredError, "Configuration already set, cannot disable") + end + + it "raises AlreadyConfiguredError when default is called after disable!" do + Configuration.disable! + + expect { + Configuration.default do |config| + config.csp = { default_src: %w('self'), script_src: %w('self') } + end + }.to raise_error(Configuration::AlreadyConfiguredError, "Configuration has been disabled, cannot set default") + end + + it "allows default to be called after disable! and reset_config" do + Configuration.disable! + reset_config + + expect { + Configuration.default do |config| + config.csp = { default_src: %w('self'), script_src: %w('self') } + end + }.not_to raise_error + + # After reset_config, disabled? returns nil (not false) because @disabled is removed + expect(Configuration.disabled?).to be_falsy + expect(Configuration.instance_variable_defined?(:@default_config)).to be true + end + + it "works correctly with dup when library is disabled" do + Configuration.disable! + config = Configuration.dup + + Configuration::CONFIG_ATTRIBUTES.each do |attr| + expect(config.instance_variable_get("@#{attr}")).to eq(OPT_OUT) + end + end + + it "does not interfere with override mechanism" do + Configuration.disable! + + # Should be able to use opt_out_of_all_protection without error + request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") + expect { + SecureHeaders.opt_out_of_all_protection(request) + }.not_to raise_error + end + + it "interacts correctly with named overrides when disabled" do + Configuration.disable! + + Configuration.override(:test_override) do |config| + config.x_frame_options = "DENY" + end + + expect(Configuration.overrides(:test_override)).to_not be_nil + end + end end end diff --git a/spec/lib/secure_headers/headers/content_security_policy_spec.rb b/spec/lib/secure_headers/headers/content_security_policy_spec.rb index c16e70a2..fc6ae5db 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -48,12 +48,20 @@ module SecureHeaders expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:") end + it "normalizes source expressions that end with a trailing /" do + config = { + default_src: %w(a.example.org/ b.example.com/ wss://c.example.com/ c.example.net/foo/ b.example.co/bar wss://b.example.co/) + } + csp = ContentSecurityPolicy.new(config) + expect(csp.value).to eq("default-src a.example.org b.example.com wss://c.example.com c.example.net/foo/ b.example.co/bar wss://b.example.co") + end + it "does not minify source expressions based on overlapping wildcards" do config = { - default_src: %w(a.example.org b.example.org *.example.org https://*.example.org) + default_src: %w(a.example.org b.example.org *.example.org https://*.example.org c.example.org/) } csp = ContentSecurityPolicy.new(config) - expect(csp.value).to eq("default-src a.example.org b.example.org *.example.org") + expect(csp.value).to eq("default-src a.example.org b.example.org *.example.org c.example.org") end it "removes http/s schemes from hosts" do @@ -210,6 +218,31 @@ module SecureHeaders csp = ContentSecurityPolicy.new({ trusted_types: %w(blahblahpolicy 'allow-duplicates') }) expect(csp.value).to eq("trusted-types blahblahpolicy 'allow-duplicates'") end + + it "supports report-to directive with endpoint name" do + csp = ContentSecurityPolicy.new({ default_src: %w('self'), report_to: "csp-endpoint" }) + expect(csp.value).to eq("default-src 'self'; report-to csp-endpoint") + end + + it "includes report-to before report-uri in alphabetical order" do + csp = ContentSecurityPolicy.new({ default_src: %w('self'), report_uri: %w(/csp_report), report_to: "csp-endpoint" }) + expect(csp.value).to eq("default-src 'self'; report-to csp-endpoint; report-uri /csp_report") + end + + it "does not add report-to if the endpoint name is empty" do + csp = ContentSecurityPolicy.new({ default_src: %w('self'), report_to: "" }) + expect(csp.value).to eq("default-src 'self'") + end + + it "does not add report-to if not provided" do + csp = ContentSecurityPolicy.new({ default_src: %w('self') }) + expect(csp.value).not_to include("report-to") + end + + it "supports report-to without report-uri" do + csp = ContentSecurityPolicy.new({ default_src: %w('self'), report_to: "reporting-endpoint-name" }) + expect(csp.value).to eq("default-src 'self'; report-to reporting-endpoint-name") + end end end end diff --git a/spec/lib/secure_headers/headers/policy_management_spec.rb b/spec/lib/secure_headers/headers/policy_management_spec.rb index 99065744..626a1a82 100644 --- a/spec/lib/secure_headers/headers/policy_management_spec.rb +++ b/spec/lib/secure_headers/headers/policy_management_spec.rb @@ -169,6 +169,30 @@ module SecureHeaders ContentSecurityPolicy.validate_config!(ContentSecurityPolicyReportOnlyConfig.new(default_opts.merge(report_only: true))) end.to_not raise_error end + + it "requires report_to to be a string" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_to: ["endpoint"]))) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "rejects empty report_to endpoint names" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_to: ""))) + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "accepts valid report_to endpoint names" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_to: "csp-endpoint"))) + end.to_not raise_error + end + + it "accepts report_to with hyphens and underscores" do + expect do + ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_to: "csp-endpoint_name-123"))) + end.to_not raise_error + end end describe "#combine_policies" do diff --git a/spec/lib/secure_headers/headers/reporting_endpoints_spec.rb b/spec/lib/secure_headers/headers/reporting_endpoints_spec.rb new file mode 100644 index 00000000..a57dda05 --- /dev/null +++ b/spec/lib/secure_headers/headers/reporting_endpoints_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true +require "spec_helper" + +module SecureHeaders + describe ReportingEndpoints do + describe "#make_header" do + it "returns nil when config is nil" do + expect(ReportingEndpoints.make_header(nil)).to be_nil + end + + it "returns nil when config is OPT_OUT" do + expect(ReportingEndpoints.make_header(OPT_OUT)).to be_nil + end + + it "formats a single endpoint" do + config = { "csp-endpoint" => "https://example.com/csp-reports" } + header_name, value = ReportingEndpoints.make_header(config) + expect(header_name).to eq("reporting-endpoints") + expect(value).to eq('csp-endpoint="https://example.com/csp-reports"') + end + + it "formats a single endpoint with a symbol" do + config = { "csp-endpoint": "https://example.com/csp-reports" } + header_name, value = ReportingEndpoints.make_header(config) + expect(header_name).to eq("reporting-endpoints") + expect(value).to eq('csp-endpoint="https://example.com/csp-reports"') + end + + it "formats multiple endpoints" do + config = { + "csp-endpoint" => "https://example.com/csp-reports", + "permissions-endpoint" => "https://example.com/permissions-reports" + } + header_name, value = ReportingEndpoints.make_header(config) + expect(header_name).to eq("reporting-endpoints") + # Order may vary, so check both endpoints are present + expect(value).to include('csp-endpoint="https://example.com/csp-reports"') + expect(value).to include('permissions-endpoint="https://example.com/permissions-reports"') + expect(value).to include(",") + end + + it "validates that endpoints are present" do + expect do + ReportingEndpoints.validate_config!({}) + end.to_not raise_error + end + end + + describe "#validate_config!" do + it "accepts nil" do + expect do + ReportingEndpoints.validate_config!(nil) + end.to_not raise_error + end + + it "accepts OPT_OUT" do + expect do + ReportingEndpoints.validate_config!(OPT_OUT) + end.to_not raise_error + end + + it "accepts valid endpoint configuration" do + expect do + ReportingEndpoints.validate_config!({ + "csp-violations" => "https://example.com/reports" + }) + end.to_not raise_error + end + + it "accepts valid endpoint configuration with symbol keys" do + expect do + ReportingEndpoints.validate_config!({ + "csp-violations": "https://example.com/reports" + }) + end.to_not raise_error + end + + it "rejects non-hash config" do + expect do + ReportingEndpoints.validate_config!("not a hash") + end.to raise_error(TypeError) + end + + it "rejects empty endpoint name" do + expect do + ReportingEndpoints.validate_config!({ + "" => "https://example.com/reports" + }) + end.to raise_error(ReportingEndpointsConfigError) + end + + it "rejects non-string endpoint name" do + expect do + ReportingEndpoints.validate_config!({ + 123 => "https://example.com/reports" + }) + end.to raise_error(ReportingEndpointsConfigError) + end + + it "rejects empty endpoint URL" do + expect do + ReportingEndpoints.validate_config!({ + "csp-endpoint" => "" + }) + end.to raise_error(ReportingEndpointsConfigError) + end + + it "rejects non-string endpoint URL" do + expect do + ReportingEndpoints.validate_config!({ + "csp-endpoint" => 123 + }) + end.to raise_error(ReportingEndpointsConfigError) + end + + it "rejects non-https URLs" do + expect do + ReportingEndpoints.validate_config!({ + "csp-endpoint" => "http://example.com/reports" + }) + end.to raise_error(ReportingEndpointsConfigError, /must use https/) + end + end + end +end diff --git a/spec/lib/secure_headers/middleware_spec.rb b/spec/lib/secure_headers/middleware_spec.rb index f019b597..dd5100eb 100644 --- a/spec/lib/secure_headers/middleware_spec.rb +++ b/spec/lib/secure_headers/middleware_spec.rb @@ -36,6 +36,27 @@ module SecureHeaders expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org") end + context "Rack::Headers conversion" do + it "converts headers to Rack::Headers when Rack::Headers is defined" do + if defined?(Rack::Headers) + _, headers, = middleware.call(Rack::MockRequest.env_for("https://localhost", {})) + expect(headers.is_a?(Rack::Headers)).to be true + else + skip "Rack::Headers is not defined in this version of Rack" + end + end + + it "keeps headers as a hash when Rack::Headers is not defined" do + # Temporarily hide the Rack::Headers constant if it exists + if defined?(Rack::Headers) + hide_const("Rack::Headers") + end + + _, headers, = middleware.call(Rack::MockRequest.env_for("https://localhost", {})) + expect(headers.is_a?(Hash)).to be true + end + end + context "cookies" do before(:each) do reset_config @@ -83,7 +104,7 @@ module SecureHeaders end it "flags cookies with a combination of SameSite configurations" do - cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] }) + cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => "_session=foobar\n_guest=true"), "app"] }) Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT } } request = Rack::Request.new("HTTPS" => "on") @@ -93,6 +114,16 @@ module SecureHeaders expect(env["Set-Cookie"]).to match("_guest=true; SameSite=Lax") end + it "keeps cookies as array after flagging if they are already an array" do + cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] }) + + Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT } } + request = Rack::Request.new("HTTPS" => "on") + _, env = cookie_middleware.call request.env + + expect(env["Set-Cookie"]).to match_array(["_session=foobar; SameSite=Strict", "_guest=true; SameSite=Lax"]) + end + it "disables secure cookies for non-https requests" do Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } } @@ -113,5 +144,33 @@ module SecureHeaders expect(env["Set-Cookie"]).to eq("foo=bar; secure") end end + + context "when disabled" do + before(:each) do + reset_config + Configuration.disable! + end + + it "does not set any headers" do + _, env = middleware.call(Rack::MockRequest.env_for("https://localhost", {})) + + # Verify no security headers are set by checking all configured header classes + Configuration::HEADERABLE_ATTRIBUTES.each do |attr| + klass = Configuration::CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr] + # Handle CSP specially since it has multiple classes + if attr == :csp + expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil + expect(env[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil + elsif klass.const_defined?(:HEADER_NAME) + expect(env[klass::HEADER_NAME]).to be_nil + end + end + end + + it "does not flag cookies" do + _, env = cookie_middleware.call(Rack::MockRequest.env_for("https://localhost", {})) + expect(env["Set-Cookie"]).to eq("foo=bar") + end + end end end diff --git a/spec/lib/secure_headers/railtie_spec.rb b/spec/lib/secure_headers/railtie_spec.rb new file mode 100644 index 00000000..37c11bce --- /dev/null +++ b/spec/lib/secure_headers/railtie_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true +require "spec_helper" + +describe "SecureHeaders::Railtie" do + # Store initializer blocks so we can execute them in tests + let(:initializer_blocks) { {} } + let(:default_headers) { {} } + + before do + # Clean up any previous Rails/ActiveSupport definitions + Object.send(:remove_const, :Rails) if defined?(Rails) + Object.send(:remove_const, :ActiveSupport) if defined?(ActiveSupport) + + # Remove the SecureHeaders::Railtie if it was previously defined + SecureHeaders.send(:remove_const, :Railtie) if defined?(SecureHeaders::Railtie) + + blocks = initializer_blocks + headers = default_headers + + # Mock ActiveSupport.on_load to immediately execute the block + stub_const("ActiveSupport", Module.new do + define_singleton_method(:on_load) do |name, &block| + block.call if block + end + end) + + # Create mock Rails module with application config + rails_module = Module.new do + # Create a mock Railtie base class + const_set(:Railtie, Class.new do + define_singleton_method(:isolate_namespace) { |*| } + define_singleton_method(:initializer) do |name, &block| + blocks[name] = block + end + define_singleton_method(:rake_tasks) { |&block| } + end) + + # Create mock application with config + config_action_dispatch = Struct.new(:default_headers).new(headers) + config_middleware = Object.new.tap do |mw| + mw.define_singleton_method(:insert_before) { |*| } + end + config = Struct.new(:action_dispatch, :middleware).new(config_action_dispatch, config_middleware) + application = Struct.new(:config).new(config) + + define_singleton_method(:application) { application } + end + + stub_const("Rails", rails_module) + end + + def load_railtie + # Load the railtie file fresh + load File.expand_path("../../../../lib/secure_headers/railtie.rb", __FILE__) + end + + def run_action_controller_initializer + initializer_blocks["secure_headers.action_controller"]&.call + end + + describe "case-insensitive header removal" do + context "with Rails default headers (capitalized format like 'X-Frame-Options')" do + let(:default_headers) do + { + "X-Frame-Options" => "SAMEORIGIN", + "X-XSS-Protection" => "0", + "X-Content-Type-Options" => "nosniff", + "X-Permitted-Cross-Domain-Policies" => "none", + "X-Download-Options" => "noopen", + "Referrer-Policy" => "strict-origin-when-cross-origin" + } + end + + it "removes capitalized conflicting headers from Rails defaults" do + load_railtie + run_action_controller_initializer + + expect(default_headers).to be_empty + end + end + + context "with lowercase headers (Rack 3+ format)" do + let(:default_headers) do + { + "x-frame-options" => "SAMEORIGIN", + "x-xss-protection" => "0", + "x-content-type-options" => "nosniff" + } + end + + it "removes lowercase conflicting headers from Rails defaults" do + load_railtie + run_action_controller_initializer + + expect(default_headers).to be_empty + end + end + + context "with mixed-case headers" do + let(:default_headers) do + { + "X-FRAME-OPTIONS" => "SAMEORIGIN", + "x-Xss-Protection" => "0", + "X-Content-Type-OPTIONS" => "nosniff" + } + end + + it "removes mixed-case conflicting headers from Rails defaults" do + load_railtie + run_action_controller_initializer + + expect(default_headers).to be_empty + end + end + + context "preserving non-conflicting headers" do + let(:default_headers) do + { + "X-Frame-Options" => "SAMEORIGIN", + "X-Custom-Header" => "custom-value", + "My-Application-Header" => "app-value" + } + end + + it "removes only conflicting headers and preserves custom headers" do + load_railtie + run_action_controller_initializer + + expect(default_headers).to eq({ + "X-Custom-Header" => "custom-value", + "My-Application-Header" => "app-value" + }) + end + end + + context "with nil default_headers" do + let(:default_headers) { nil } + + it "handles nil default_headers gracefully" do + load_railtie + + expect { run_action_controller_initializer }.not_to raise_error + end + end + + context "CSP and HSTS headers" do + let(:default_headers) do + { + "Content-Security-Policy" => "default-src 'self'", + "Content-Security-Policy-Report-Only" => "default-src 'self'", + "Strict-Transport-Security" => "max-age=31536000" + } + end + + it "removes CSP and HSTS headers regardless of case" do + load_railtie + run_action_controller_initializer + + expect(default_headers).to be_empty + end + end + end +end diff --git a/spec/lib/secure_headers/task_helper_spec.rb b/spec/lib/secure_headers/task_helper_spec.rb new file mode 100644 index 00000000..8be633f5 --- /dev/null +++ b/spec/lib/secure_headers/task_helper_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true +require "spec_helper" +require "secure_headers/task_helper" + +class TestHelper + include SecureHeaders::TaskHelper +end + +module SecureHeaders + describe TaskHelper do + subject { TestHelper.new } + + let(:template) do + < + + + + <%= hashed_javascript_tag do %> + alert("Using the helper tag!") + <% end %> + <%= hashed_style_tag do %> + p { text-decoration: underline; } + <% end %> + + +

Testing

+ + +EOT + end + + let(:template_unindented) do + < + + + + <%= hashed_javascript_tag do %> + alert("Using the helper tag!") +<% end %> + <%= hashed_style_tag do %> + p { text-decoration: underline; } +<% end %> + + +

Testing

+ + +EOT + end + + describe "#generate_inline_script_hashes" do + let(:expected_hashes) do + [ + "'sha256-EE/znQZ7BcfM3LbsqxUc5JlCtE760Pc2RV18tW90DCo='", + "'sha256-64ro9ciexeO5JqSZcAnhmJL4wbzCrpsZJLWl5H6mrkA='" + ] + end + + it "returns an array of found script hashes" do + Tempfile.create("script") do |f| + f.write template + f.flush + expect(subject.generate_inline_script_hashes(f.path)).to eq expected_hashes + end + end + it "returns the same array no matter the indentation of helper end tags" do + Tempfile.create("script") do |f| + f.write template_unindented + f.flush + expect(subject.generate_inline_script_hashes(f.path)).to eq expected_hashes + end + end + end + + describe "#generate_inline_style_hashes" do + let(:expected_hashes) do + [ + "'sha256-pckGv9YvNcB5xy+Y4fbqhyo+ib850wyiuWeNbZvLi00='", + "'sha256-d374zYt40cLTr8J7Cvm/l4oDY4P9UJ8TWhYG0iEglU4='" + ] + end + + it "returns an array of found style hashes" do + Tempfile.create("style") do |f| + f.write template + f.flush + expect(subject.generate_inline_style_hashes(f.path)).to eq expected_hashes + end + end + it "returns the same array no matter the indentation of helper end tags" do + Tempfile.create("style") do |f| + f.write template_unindented + f.flush + expect(subject.generate_inline_style_hashes(f.path)).to eq expected_hashes + end + end + end + + describe "#dynamic_content?" do + context "mustache file" do + it "finds mustache templating tokens" do + expect(subject.dynamic_content?("file.mustache", "var test = {{ dynamic_value }};")).to be true + end + + it "returns false when not finding any templating tokens" do + expect(subject.dynamic_content?("file.mustache", "var test = 'static value';")).to be false + end + end + + context "erb file" do + it "finds erb templating tokens" do + expect(subject.dynamic_content?("file.erb", "var test = <%= dynamic_value %>;")).to be true + end + + it "returns false when not finding any templating tokens" do + expect(subject.dynamic_content?("file.erb", "var test = 'static value';")).to be false + end + end + end + end +end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index 843b9f1e..4cc5255b 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -112,6 +112,12 @@ module SecureHeaders expect(hash.count).to eq(0) end + it "allows you to disable secure_headers entirely via Configuration.disable!" do + Configuration.disable! + hash = SecureHeaders.header_hash_for(request) + expect(hash.count).to eq(0) + end + it "allows you to override x-frame-options settings" do Configuration.default SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY) @@ -436,6 +442,68 @@ module SecureHeaders end end + + it "does not set upgrade-insecure-requests if request is over HTTP" do + reset_config + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self'), + upgrade_insecure_requests: true + } + end + + plaintext_request = Rack::Request.new({}) + hash = SecureHeaders.header_hash_for(plaintext_request) + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src 'self'") + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).not_to include("upgrade-insecure-requests") + end + + it "sets upgrade-insecure-requests if request is over HTTPS" do + reset_config + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self'), + upgrade_insecure_requests: true + } + end + + https_request = Rack::Request.new("HTTPS" => "on") + hash = SecureHeaders.header_hash_for(https_request) + expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src 'self'; upgrade-insecure-requests") + end + + it "does not set upgrade-insecure-requests in report-only mode if request is over HTTP" do + reset_config + Configuration.default do |config| + config.csp_report_only = { + default_src: %w('self'), + script_src: %w('self'), + upgrade_insecure_requests: true + } + end + + plaintext_request = Rack::Request.new({}) + hash = SecureHeaders.header_hash_for(plaintext_request) + expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src 'self'") + expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).not_to include("upgrade-insecure-requests") + end + + it "sets upgrade-insecure-requests in report-only mode if request is over HTTPS" do + reset_config + Configuration.default do |config| + config.csp_report_only = { + default_src: %w('self'), + script_src: %w('self'), + upgrade_insecure_requests: true + } + end + + https_request = Rack::Request.new("HTTPS" => "on") + hash = SecureHeaders.header_hash_for(https_request) + expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src 'self'; upgrade-insecure-requests") + end end end @@ -527,6 +595,146 @@ module SecureHeaders end end.to raise_error(CookiesConfigError) end + + it "validates report_to directive on configuration" do + expect do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self'), + report_to: ["not_a_string"] + } + end + end.to raise_error(ContentSecurityPolicyConfigError) + end + + it "allows report_to directive with string endpoint" do + expect do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self'), + report_to: "csp-endpoint" + } + end + end.to_not raise_error + end + end + + describe "report_to with overrides and appends" do + let(:request) { double("Request", scheme: "https", env: {}) } + + it "overrides the report_to directive" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self'), + report_to: "endpoint-1" + } + end + + SecureHeaders.override_content_security_policy_directives(request, report_to: "endpoint-2") + headers = SecureHeaders.header_hash_for(request) + csp_header = headers[ContentSecurityPolicyConfig::HEADER_NAME] + expect(csp_header).to include("report-to endpoint-2") + end + + it "includes report_to when appending CSP directives" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self') + } + end + + SecureHeaders.append_content_security_policy_directives(request, report_to: "new-endpoint") + headers = SecureHeaders.header_hash_for(request) + csp_header = headers[ContentSecurityPolicyConfig::HEADER_NAME] + expect(csp_header).to include("report-to new-endpoint") + end + + it "handles report_to with report_uri together" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self'), + report_uri: %w(/csp-report), + report_to: "reporting-endpoint" + } + end + + headers = SecureHeaders.header_hash_for(request) + csp_header = headers[ContentSecurityPolicyConfig::HEADER_NAME] + # Both should be present + expect(csp_header).to include("report-to reporting-endpoint") + expect(csp_header).to include("report-uri /csp-report") + # report-to should come before report-uri (alphabetical order) + expect(csp_header.index("report-to")).to be < csp_header.index("report-uri") + end + end + + describe "reporting_endpoints header generation" do + let(:request) { double("Request", scheme: "https", env: {}) } + + before(:each) do + reset_config + end + + it "includes reporting_endpoints header in generated headers" do + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self') + } + config.reporting_endpoints = { + "csp-endpoint" => "https://example.com/reports" + } + end + + headers = SecureHeaders.header_hash_for(request) + expect(headers["reporting-endpoints"]).to eq('csp-endpoint="https://example.com/reports"') + end + + it "includes reporting_endpoints after config.dup() is called" do + # This test specifically validates that reporting_endpoints survives + # the .dup() call made by the middleware + Configuration.default do |config| + config.csp = { + default_src: %w('self'), + script_src: %w('self') + } + config.reporting_endpoints = { + "csp-violations" => "https://api.example.com/reports?enforcement=enforce", + "csp-violations-report-only" => "https://api.example.com/reports?enforcement=report-only" + } + end + + # Simulate what the middleware does internally + config = Configuration.dup # ← This calls .dup() which must preserve reporting_endpoints + headers = config.generate_headers + + expect(headers["reporting-endpoints"]).to include('csp-violations="https://api.example.com/reports?enforcement=enforce"') + expect(headers["reporting-endpoints"]).to include('csp-violations-report-only="https://api.example.com/reports?enforcement=report-only"') + end + + it "does not include reporting_endpoints header when OPT_OUT" do + Configuration.default do |config| + config.csp = { default_src: %w('self'), script_src: %w('self') } + config.reporting_endpoints = OPT_OUT + end + + headers = SecureHeaders.header_hash_for(request) + expect(headers["reporting-endpoints"]).to be_nil + end + + it "does not include reporting_endpoints header when not configured" do + Configuration.default do |config| + config.csp = { default_src: %w('self'), script_src: %w('self') } + end + + headers = SecureHeaders.header_hash_for(request) + expect(headers["reporting-endpoints"]).to be_nil + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 65627eec..0685312d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -52,6 +52,11 @@ def self.clear_overrides def self.clear_appends remove_instance_variable(:@appends) if defined?(@appends) end + + def self.clear_disabled + remove_instance_variable(:@disabled) if defined?(@disabled) + remove_instance_variable(:@noop_config) if defined?(@noop_config) + end end end @@ -59,4 +64,5 @@ def reset_config SecureHeaders::Configuration.clear_default_config SecureHeaders::Configuration.clear_overrides SecureHeaders::Configuration.clear_appends + SecureHeaders::Configuration.clear_disabled end From 91bb8e3199fcc117722bd541e7f72eaec207156f Mon Sep 17 00:00:00 2001 From: Dennis Pacewicz <119963426+rei-moo@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:45:18 +0000 Subject: [PATCH 632/636] update release workflow to publish ruby gem automatically --- .github/workflows/github-release.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index ef6018a2..3b367d01 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -8,6 +8,7 @@ on: jobs: Publish: permissions: + id-token: write contents: write runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') @@ -26,3 +27,12 @@ jobs: release_name: ${{ env.RELEASE_NAME }} draft: false prerelease: false + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Set up Ruby + uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd + with: + bundler-cache: true + ruby-version: ruby + - uses: rubygems/release-gem@v1 From 920e2badbd2c5069a14bd943a76e04c18c46682c Mon Sep 17 00:00:00 2001 From: Dennis Pacewicz <119963426+rei-moo@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:50:34 +0900 Subject: [PATCH 633/636] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/github-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 3b367d01..c9654dad 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -31,7 +31,7 @@ jobs: with: persist-credentials: false - name: Set up Ruby - uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd + uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd with: bundler-cache: true ruby-version: ruby From 4111d495e858874caff9db2a6ae9100accd998d5 Mon Sep 17 00:00:00 2001 From: Dennis Pacewicz <119963426+rei-moo@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:51:49 +0000 Subject: [PATCH 634/636] fix --- .github/workflows/github-release.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index c9654dad..78621016 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -13,6 +13,13 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Set up Ruby + uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd + with: + bundler-cache: true - name: Calculate release name run: | GITHUB_REF=${{ github.ref }} @@ -27,12 +34,4 @@ jobs: release_name: ${{ env.RELEASE_NAME }} draft: false prerelease: false - - uses: actions/checkout@v5 - with: - persist-credentials: false - - name: Set up Ruby - uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd - with: - bundler-cache: true - ruby-version: ruby - uses: rubygems/release-gem@v1 From f224144c99002bcd3c06ed86c169429d4be1e5dc Mon Sep 17 00:00:00 2001 From: Matt Langlois Date: Fri, 20 Feb 2026 13:36:55 -0700 Subject: [PATCH 635/636] Bump Version to 7.2.0 (#581) Bump the release version to `7.2.0` and bump the ruby version so we can use `release-gem` to publish the gem to rubygems. --- .ruby-version | 2 +- lib/secure_headers/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ruby-version b/.ruby-version index 8a4b2758..10fa79aa 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.6 \ No newline at end of file +3.4.8 \ No newline at end of file diff --git a/lib/secure_headers/version.rb b/lib/secure_headers/version.rb index 894200a2..b1914cac 100644 --- a/lib/secure_headers/version.rb +++ b/lib/secure_headers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SecureHeaders - VERSION = "7.1.0" + VERSION = "7.2.0" end From f9b0dd26cad7c8fb277a2f19ebbb4f9bf3bcf5a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:01:18 +0000 Subject: [PATCH 636/636] Bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/github-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 78621016..4265b809 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Ruby