Merge pull request #11 from twitter/two_headers

Add ability to apply two headers support tuning policies through experimtation
This commit is contained in:
Neil Matatall 2013-02-01 17:11:55 -08:00
Родитель a296ee1c1d d1cccc1366
Коммит 118a66bfe7
6 изменённых файлов: 126 добавлений и 8 удалений

Просмотреть файл

@ -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" }

Просмотреть файл

@ -134,6 +134,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'
}
}
}
```

Просмотреть файл

@ -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)

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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