From d1cccc13660c06545616f571aa337cda0b6ee0c4 Mon Sep 17 00:00:00 2001 From: Neil Matatall Date: Thu, 31 Jan 2013 15:42:34 -0800 Subject: [PATCH] Add ability to apply two headers support tuning policies through experimentation --- Guardfile | 2 +- README.md | 13 ++++ lib/secure_headers.rb | 4 + .../headers/content_security_policy.rb | 13 +++- .../headers/content_security_policy_spec.rb | 75 +++++++++++++++++-- spec/lib/secure_headers_spec.rb | 27 +++++++ 6 files changed, 126 insertions(+), 8 deletions(-) diff --git a/Guardfile b/Guardfile index f2d97a1..3998394 100644 --- a/Guardfile +++ b/Guardfile @@ -2,7 +2,7 @@ guard 'spork', :rspec_env => { 'RAILS_ENV' => 'test' } do watch('spec/spec_helper.rb') { :rspec } end -guard 'rspec', :cli => "--color --drb", :keep_failed => true, :all_after_pass => true do +guard 'rspec', :cli => "--color --drb", :keep_failed => true, :all_after_pass => true, :focus_on_failed => true do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } watch(%r{^app/controllers/(.+)\.rb$}) { |m| "spec/controllers/#{m[1]}_spec.rb" } diff --git a/README.md b/README.md index 45ef541..9bf855f 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,19 @@ and [Firefox CSP specification](https://wiki.mozilla.org/Security/CSP/Specificat # would produce the directive: "img-src https://* http://*;" # when over http, ignored for https requests :http_additions => {} + + # If you have enforce => true, you can use the `experiments` block to + # also produce a report-only header. Values in this block override the + # parent config for the report-only, and leave the enforcing header + # unaltered. http_additions work the same way described above, but + # are added to your report-only header as expected. + :experimental => { + :script_src => 'self', + :img_src => 'https://mycdn.example.com', + :http_additions { + :img_src => 'http://mycdn.example.com' + } + } } ``` diff --git a/lib/secure_headers.rb b/lib/secure_headers.rb index 8e1385d..4e10ea1 100644 --- a/lib/secure_headers.rb +++ b/lib/secure_headers.rb @@ -53,6 +53,10 @@ module SecureHeaders header = ContentSecurityPolicy.new(request, options) set_header(header.name, header.value) + if options && options[:experimental] && options[:enforce] + header = ContentSecurityPolicy.new(request, options, :experimental => true) + set_header(header.name, header.value) + end end def set_a_header(name, klass, options=nil) diff --git a/lib/secure_headers/headers/content_security_policy.rb b/lib/secure_headers/headers/content_security_policy.rb index 524a958..a3266b9 100644 --- a/lib/secure_headers/headers/content_security_policy.rb +++ b/lib/secure_headers/headers/content_security_policy.rb @@ -30,7 +30,8 @@ module SecureHeaders alias :ssl_request? :ssl_request - def initialize request = nil, config = nil + def initialize(request=nil, config=nil, options={}) + @experimental = !!options.delete(:experimental) if config configure request, config elsif request @@ -41,6 +42,12 @@ module SecureHeaders def configure request, opts @config = opts.dup + experimental_config = @config.delete(:experimental) + if @experimental && experimental_config + @config[:http_additions] = experimental_config[:http_additions] + @config.merge!(experimental_config) + end + parse_request request META.each do |meta| self.send(meta.to_s + "=", @config.delete(meta)) @@ -63,7 +70,9 @@ module SecureHeaders WEBKIT_CSP_HEADER_NAME end - base += "-Report-Only" unless enforce + if !enforce || @experimental + base += "-Report-Only" + end base 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 f566ff5..0c17f8f 100644 --- a/spec/lib/secure_headers/headers/content_security_policy_spec.rb +++ b/spec/lib/secure_headers/headers/content_security_policy_spec.rb @@ -19,8 +19,8 @@ module SecureHeaders FIREFOX_18 = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/18.0 Firefox/18.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" - def request_for user_agent, request_uri = nil - double(:ssl? => false, :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://example.com') ) + 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://example.com') ) end before(:each) do @@ -43,6 +43,14 @@ module SecureHeaders specify { ContentSecurityPolicy.new(request_for(FIREFOX_18), opts).name.should == FIREFOX_CSP_HEADER_NAME} specify { ContentSecurityPolicy.new(request_for(CHROME), opts).name.should == WEBKIT_CSP_HEADER_NAME} end + + context "when in experimental mode" do + let(:opts) { default_opts.merge(:enforce => true).merge(:experimental => {})} + specify { ContentSecurityPolicy.new(request_for(IE), opts, :experimental => true).name.should == STANDARD_HEADER_NAME + "-Report-Only"} + specify { ContentSecurityPolicy.new(request_for(FIREFOX), opts, :experimental => true).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"} + specify { ContentSecurityPolicy.new(request_for(FIREFOX_18), opts, :experimental => true).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"} + specify { ContentSecurityPolicy.new(request_for(CHROME), opts, :experimental => true).name.should == WEBKIT_CSP_HEADER_NAME + "-Report-Only"} + end end describe "#fill_directives" do @@ -302,6 +310,29 @@ module SecureHeaders end end + context "when supplying a experimental values" do + let(:options) {{ + :disable_chrome_extension => true, + :disable_fill_missing => true, + :default_src => 'self', + :script_src => 'https://*', + :experimental => { + :script_src => 'self' + } + }} + + let(:header) {} + it "returns the original value" do + header = ContentSecurityPolicy.new(request_for(CHROME), options) + header.value.should == "default-src 'self'; script-src https://*;" + end + + it "it returns the experimental value if requested" do + header = ContentSecurityPolicy.new(request_for(CHROME), options, :experimental => true) + header.value.should == "default-src 'self'; script-src 'self';" + end + end + context "when supplying additional http directive values" do let(:options) { default_opts.merge({ @@ -318,11 +349,45 @@ module SecureHeaders end it "does not add the directive values if requesting https" do - request = request_for(CHROME) - request.stub(:ssl?).and_return(true) - csp = ContentSecurityPolicy.new(request, options) + csp = ContentSecurityPolicy.new(request_for(CHROME, '/', :ssl => true), options) csp.value.should == "default-src https://*; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* chrome-extension: about:; report-uri /csp_report;" end + + context "when supplying an experimental block" do + # this simulates the situation where we are enforcing that scripts + # only come from http[s]? depending if we're on ssl or not. The + # report only tag will allow scripts from self over ssl, and + # from a secure CDN over non-ssl + let(:options) {{ + :disable_chrome_extension => true, + :disable_fill_missing => true, + :default_src => 'self', + :script_src => 'https://*', + :http_additions => { + :script_src => 'http://*' + }, + :experimental => { + :script_src => 'self', + :http_additions => { + :script_src => 'https://mycdn.example.com' + } + } + }} + # for comparison purposes, if not using the experimental header this would produce + # "allow 'self'; script-src https://*" for https requests + # and + # "allow 'self; script-src https://* http://*" for http requests + + it "uses the value in the experimental block over SSL" do + csp = ContentSecurityPolicy.new(request_for(FIREFOX, '/', :ssl => true), options, :experimental => true) + csp.value.should == "allow 'self'; script-src 'self';" + end + + it "merges the values from experimental/http_additions when not over SSL" do + csp = ContentSecurityPolicy.new(request_for(FIREFOX), options, :experimental => true) + csp.value.should == "allow 'self'; script-src 'self' https://mycdn.example.com;" + end + end end end end diff --git a/spec/lib/secure_headers_spec.rb b/spec/lib/secure_headers_spec.rb index e19ad9a..5f783e7 100644 --- a/spec/lib/secure_headers_spec.rb +++ b/spec/lib/secure_headers_spec.rb @@ -221,5 +221,32 @@ describe SecureHeaders do subject.set_csp_header request end end + + context "when using the experimental key" do + before(:each) do + stub_user_agent(USER_AGENTS[:chrome]) + @opts = { + :enforce => true, + :default_src => 'self', + :script_src => 'https://mycdn.example.com', + :experimental => { + :script_src => 'self', + } + } + end + + it "does not set the header in enforce mode if experimental is supplied, but enforce is disabled" do + opts = @opts.merge(:enforce => false) + should_assign_header(WEBKIT_CSP_HEADER_NAME + "-Report-Only", anything) + should_not_assign_header(WEBKIT_CSP_HEADER_NAME) + subject.set_csp_header request, opts + end + + it "sets a header in enforce mode as well as report-only mode" do + should_assign_header(WEBKIT_CSP_HEADER_NAME, anything) + should_assign_header(WEBKIT_CSP_HEADER_NAME + "-Report-Only", anything) + subject.set_csp_header request, @opts + end + end end end