secure_headers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- data/.gitignore +17 -0
- data/.rvmrc +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +13 -0
- data/Guardfile +10 -0
- data/HISTORY.md +22 -0
- data/LICENSE +202 -0
- data/README.md +318 -0
- data/Rakefile +127 -0
- data/app/controllers/content_security_policy_controller.rb +44 -0
- data/config/curl-ca-bundle.crt +5420 -0
- data/config/routes.rb +3 -0
- data/lib/secure_headers.rb +115 -0
- data/lib/secure_headers/headers/content_security_policy.rb +300 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +53 -0
- data/lib/secure_headers/headers/x_content_type_options.rb +40 -0
- data/lib/secure_headers/headers/x_frame_options.rb +40 -0
- data/lib/secure_headers/headers/x_xss_protection.rb +54 -0
- data/lib/secure_headers/railtie.rb +37 -0
- data/lib/secure_headers/version.rb +3 -0
- data/secure-headers.gemspec +23 -0
- data/spec/controllers/content_security_policy_controller_spec.rb +74 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +382 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +66 -0
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +35 -0
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +38 -0
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +41 -0
- data/spec/lib/secure_headers_spec.rb +252 -0
- data/spec/spec_helper.rb +25 -0
- metadata +116 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module SecureHeaders
|
2
|
+
class XContentTypeOptionsBuildError < StandardError; end
|
3
|
+
# IE only
|
4
|
+
class XContentTypeOptions
|
5
|
+
module Constants
|
6
|
+
X_CONTENT_TYPE_OPTIONS_HEADER_NAME = "X-Content-Type-Options"
|
7
|
+
DEFAULT_VALUE = "nosniff"
|
8
|
+
end
|
9
|
+
include Constants
|
10
|
+
|
11
|
+
def initialize(config=nil)
|
12
|
+
@config = config
|
13
|
+
validate_config unless @config.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
X_CONTENT_TYPE_OPTIONS_HEADER_NAME
|
18
|
+
end
|
19
|
+
|
20
|
+
def value
|
21
|
+
case @config
|
22
|
+
when NilClass
|
23
|
+
DEFAULT_VALUE
|
24
|
+
when String
|
25
|
+
@config
|
26
|
+
else
|
27
|
+
@config[:value]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def validate_config
|
34
|
+
value = @config.is_a?(Hash) ? @config[:value] : @config
|
35
|
+
unless value.casecmp(DEFAULT_VALUE) == 0
|
36
|
+
raise XContentTypeOptionsBuildError.new("Value can only be nil or 'nosniff'")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module SecureHeaders
|
2
|
+
class XFOBuildError < StandardError; end
|
3
|
+
class XFrameOptions
|
4
|
+
module Constants
|
5
|
+
XFO_HEADER_NAME = "X-FRAME-OPTIONS"
|
6
|
+
DEFAULT_VALUE = 'SAMEORIGIN'
|
7
|
+
VALID_XFO_HEADER = /\A(SAMEORIGIN\z|DENY\z|ALLOW-FROM:)/i
|
8
|
+
end
|
9
|
+
include Constants
|
10
|
+
|
11
|
+
def initialize(config = nil)
|
12
|
+
@config = config
|
13
|
+
validate_config unless @config.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
XFO_HEADER_NAME
|
18
|
+
end
|
19
|
+
|
20
|
+
def value
|
21
|
+
case @config
|
22
|
+
when NilClass
|
23
|
+
DEFAULT_VALUE
|
24
|
+
when String
|
25
|
+
@config
|
26
|
+
else
|
27
|
+
@config[:value]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def validate_config
|
34
|
+
value = @config.is_a?(Hash) ? @config[:value] : @config
|
35
|
+
unless value =~ VALID_XFO_HEADER
|
36
|
+
raise XFOBuildError.new("Value must be SAMEORIGIN|DENY|ALLOW-FROM:")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module SecureHeaders
|
2
|
+
class XXssProtectionBuildError < StandardError; end
|
3
|
+
# IE only
|
4
|
+
class XXssProtection
|
5
|
+
module Constants
|
6
|
+
X_XSS_PROTECTION_HEADER_NAME = 'X-XSS-Protection'
|
7
|
+
DEFAULT_VALUE = "1"
|
8
|
+
VALID_X_XSS_HEADER = /\A[01](; mode=block)?\z/i
|
9
|
+
end
|
10
|
+
include Constants
|
11
|
+
|
12
|
+
def initialize(config=nil)
|
13
|
+
@config = config
|
14
|
+
validate_config unless @config.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
X_XSS_PROTECTION_HEADER_NAME
|
19
|
+
end
|
20
|
+
|
21
|
+
def value
|
22
|
+
case @config
|
23
|
+
when NilClass
|
24
|
+
DEFAULT_VALUE
|
25
|
+
when String
|
26
|
+
@config
|
27
|
+
else
|
28
|
+
value = @config[:value].to_s
|
29
|
+
value += "; mode=#{@config[:mode]}" if @config[:mode]
|
30
|
+
value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validate_config
|
37
|
+
if @config.is_a? Hash
|
38
|
+
if !@config[:value]
|
39
|
+
raise XXssProtectionBuildError.new(":value key is missing")
|
40
|
+
elsif @config[:value]
|
41
|
+
unless [0,1].include?(@config[:value])
|
42
|
+
raise XXssProtectionBuildError.new(":value must be 1 or 0")
|
43
|
+
end
|
44
|
+
|
45
|
+
if @config[:mode] && @config[:mode].casecmp('block') != 0
|
46
|
+
raise XXssProtectionBuildError.new(":mode must nil or 'block'")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
elsif @config.is_a? String
|
50
|
+
raise XXssProtectionBuildError.new("Invalid format (see VALID_X_XSS_HEADER)") unless @config =~ VALID_X_XSS_HEADER
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# rails 3.1+
|
2
|
+
if defined?(Rails::Railtie)
|
3
|
+
module SecureHeaders
|
4
|
+
class Railtie < Rails::Engine
|
5
|
+
isolate_namespace ::SecureHeaders if defined? isolate_namespace # rails 3.0
|
6
|
+
ActionController::Base.send :include, ::SecureHeaders
|
7
|
+
end
|
8
|
+
end
|
9
|
+
else
|
10
|
+
module ActionController
|
11
|
+
class Base
|
12
|
+
include ::SecureHeaders
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module SecureHeaders
|
17
|
+
module Routing
|
18
|
+
module MapperExtensions
|
19
|
+
def csp_endpoint
|
20
|
+
@set.add_route(ContentSecurityPolicy::FF_CSP_ENDPOINT, {:controller => "content_security_policy", :action => "scribe"})
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if defined?(ActiveSupport::Dependencies)
|
27
|
+
if ActiveSupport::Dependencies.autoload_paths
|
28
|
+
ActiveSupport::Dependencies.autoload_paths << File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)
|
29
|
+
else
|
30
|
+
ActiveSupport::Dependencies.autoload_paths = [File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if defined? ActionController::Routing
|
35
|
+
ActionController::Routing::RouteSet::Mapper.send :include, ::SecureHeaders::Routing::MapperExtensions
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'secure_headers/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "secure_headers"
|
8
|
+
gem.version = SecureHeaders::VERSION
|
9
|
+
gem.authors = ["Neil Matatall"]
|
10
|
+
gem.email = ["neil.matatall@gmail.com"]
|
11
|
+
gem.description = %q{Add easily configured browser headers to responses.}
|
12
|
+
gem.summary = %q{Add easily configured browser headers to responses
|
13
|
+
including content security policy, x-frame-options,
|
14
|
+
strict-transport-security and more.}
|
15
|
+
gem.homepage = "https://github.com/twitter/secureheaders"
|
16
|
+
gem.license = "Apache Public License 2.0"
|
17
|
+
gem.files = `git ls-files`.split($/)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ["lib"]
|
21
|
+
gem.add_dependency "brwsr", ">= 1.1.1"
|
22
|
+
gem.add_development_dependency "rake"
|
23
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ContentSecurityPolicyController do
|
4
|
+
let(:params) {
|
5
|
+
{
|
6
|
+
"csp-report" => {
|
7
|
+
"document-uri" => "http://localhost:3001/csp","violated-directive" => "script-src 'none'",
|
8
|
+
"original-policy" => "default-src https://* 'unsafe-eval'; frame-src 'self'; img-src chrome-extension: https://*; report-uri http://localhost:3001/scribes/csp_report; script-src 'none'; style-src 'unsafe-inline' 'self';",
|
9
|
+
"blocked-uri" => "http://localhost:3001/stuff.js"
|
10
|
+
}
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
describe "#csp" do
|
15
|
+
let(:request) { double().as_null_object }
|
16
|
+
let(:endpoint) { "https://example.com" }
|
17
|
+
let(:secondary_endpoint) { "https://internal.example.com" }
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
SecureHeaders::Configuration.stub(:csp).and_return({:report_uri => endpoint, :forward_endpoint => secondary_endpoint})
|
21
|
+
subject.should_receive :head
|
22
|
+
subject.stub(:params).and_return(params)
|
23
|
+
Net::HTTP.any_instance.stub(:request)
|
24
|
+
end
|
25
|
+
|
26
|
+
context "delivery endpoint" do
|
27
|
+
it "posts over ssl" do
|
28
|
+
subject.should_receive(:use_ssl)
|
29
|
+
subject.scribe
|
30
|
+
end
|
31
|
+
|
32
|
+
it "posts over plain http" do
|
33
|
+
SecureHeaders::Configuration.stub(:csp).and_return(:report_uri => 'http://example.com')
|
34
|
+
subject.should_not_receive(:use_ssl)
|
35
|
+
subject.scribe
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it "makes a POST request" do
|
40
|
+
Net::HTTP.stub(:new).and_return(request)
|
41
|
+
request.should_receive(:request).with(instance_of(::Net::HTTP::Post))
|
42
|
+
params.stub(:to_json)
|
43
|
+
subject.scribe
|
44
|
+
end
|
45
|
+
|
46
|
+
it "POSTs to the configured forward_endpoint" do
|
47
|
+
Net::HTTP::Post.should_receive(:new).with(secondary_endpoint).and_return(request)
|
48
|
+
subject.scribe
|
49
|
+
end
|
50
|
+
|
51
|
+
it "does not POST if there is no forwarder configured" do
|
52
|
+
SecureHeaders::Configuration.stub(:csp).and_return({})
|
53
|
+
Net::HTTP::Post.should_not_receive(:new)
|
54
|
+
subject.scribe
|
55
|
+
end
|
56
|
+
|
57
|
+
it "eliminates known phony CSP reports" do
|
58
|
+
SecureHeaders::Configuration.stub(:csp).and_return(:report_uri => nil)
|
59
|
+
Net::HTTP::Post.should_not_receive :new
|
60
|
+
subject.scribe
|
61
|
+
end
|
62
|
+
|
63
|
+
it "logs errors when it cannot forward the CSP report" do
|
64
|
+
class Rails; def logger; end; end
|
65
|
+
logger = double(:repond_to? => true)
|
66
|
+
Rails.stub(:logger).and_return(logger)
|
67
|
+
|
68
|
+
SecureHeaders::Configuration.stub(:csp).and_raise(StandardError)
|
69
|
+
|
70
|
+
logger.should_receive(:warn)
|
71
|
+
subject.scribe
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,382 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'brwsr'
|
3
|
+
|
4
|
+
module SecureHeaders
|
5
|
+
describe ContentSecurityPolicy do
|
6
|
+
let(:default_opts) do
|
7
|
+
{
|
8
|
+
:disable_chrome_extension => true,
|
9
|
+
:disable_fill_missing => true,
|
10
|
+
:default_src => 'https://*',
|
11
|
+
:report_uri => '/csp_report',
|
12
|
+
:script_src => 'inline eval https://* data:',
|
13
|
+
:style_src => "inline https://* chrome-extension: about:"
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
IE = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
|
18
|
+
FIREFOX = "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8"
|
19
|
+
FIREFOX_18 = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/18.0 Firefox/18.0"
|
20
|
+
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"
|
21
|
+
|
22
|
+
def request_for user_agent, request_uri=nil, options={:ssl => false}
|
23
|
+
double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') )
|
24
|
+
end
|
25
|
+
|
26
|
+
before(:each) do
|
27
|
+
@options_with_forwarding = default_opts.merge(:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com')
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#name" do
|
31
|
+
context "when in report-only mode" do
|
32
|
+
specify { ContentSecurityPolicy.new(request_for(IE), default_opts).name.should == STANDARD_HEADER_NAME + "-Report-Only"}
|
33
|
+
specify { ContentSecurityPolicy.new(request_for(FIREFOX), default_opts).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"}
|
34
|
+
specify { ContentSecurityPolicy.new(request_for(FIREFOX_18), default_opts).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"}
|
35
|
+
specify { ContentSecurityPolicy.new(request_for(CHROME), default_opts).name.should == WEBKIT_CSP_HEADER_NAME + "-Report-Only"}
|
36
|
+
end
|
37
|
+
|
38
|
+
context "when in enforce mode" do
|
39
|
+
let(:opts) { default_opts.merge(:enforce => true)}
|
40
|
+
|
41
|
+
specify { ContentSecurityPolicy.new(request_for(IE), opts).name.should == STANDARD_HEADER_NAME}
|
42
|
+
specify { ContentSecurityPolicy.new(request_for(FIREFOX), opts).name.should == FIREFOX_CSP_HEADER_NAME}
|
43
|
+
specify { ContentSecurityPolicy.new(request_for(FIREFOX_18), opts).name.should == FIREFOX_CSP_HEADER_NAME}
|
44
|
+
specify { ContentSecurityPolicy.new(request_for(CHROME), opts).name.should == WEBKIT_CSP_HEADER_NAME}
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when in experimental mode" do
|
48
|
+
let(:opts) { default_opts.merge(:enforce => true).merge(:experimental => {})}
|
49
|
+
specify { ContentSecurityPolicy.new(request_for(IE), opts, :experimental => true).name.should == STANDARD_HEADER_NAME + "-Report-Only"}
|
50
|
+
specify { ContentSecurityPolicy.new(request_for(FIREFOX), opts, :experimental => true).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"}
|
51
|
+
specify { ContentSecurityPolicy.new(request_for(FIREFOX_18), opts, :experimental => true).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"}
|
52
|
+
specify { ContentSecurityPolicy.new(request_for(CHROME), opts, :experimental => true).name.should == WEBKIT_CSP_HEADER_NAME + "-Report-Only"}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#normalize_csp_options" do
|
57
|
+
before(:each) do
|
58
|
+
default_opts.delete(:disable_chrome_extension)
|
59
|
+
default_opts.delete(:disable_fill_missing)
|
60
|
+
default_opts[:script_src] << ' self none'
|
61
|
+
@opts = default_opts
|
62
|
+
end
|
63
|
+
|
64
|
+
context "X-Content-Security-Policy" do
|
65
|
+
it "converts the script values to their equivilents" do
|
66
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX), @opts)
|
67
|
+
csp.value.should include("script-src https://* data: 'self' 'none'")
|
68
|
+
csp.value.should include('options inline-script eval-script')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "X-Webkit-CSP" do
|
73
|
+
it "converts the script values to their equivilents" do
|
74
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME), @opts)
|
75
|
+
csp.value.should include("script-src 'unsafe-inline' 'unsafe-eval' https://* data: 'self' 'none' chrome-extension")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#build_impl_specific_directives" do
|
81
|
+
context "X-Content-Security-Policy" do
|
82
|
+
it "moves script-src inline and eval values to the options directive" do
|
83
|
+
opts = {
|
84
|
+
:default_src => 'https://*',
|
85
|
+
:script_src => "inline eval https://*"
|
86
|
+
}
|
87
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX), opts)
|
88
|
+
browser_specific = csp.send :build_impl_specific_directives
|
89
|
+
browser_specific.should include('options inline-script eval-script;')
|
90
|
+
end
|
91
|
+
|
92
|
+
it "does not move values from style-src into options" do
|
93
|
+
opts = {
|
94
|
+
:default_src => 'https://*',
|
95
|
+
:style_src => "inline eval https://*"
|
96
|
+
}
|
97
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX), opts)
|
98
|
+
browser_specific = csp.send :build_impl_specific_directives
|
99
|
+
browser_specific.should_not include('inline-script')
|
100
|
+
browser_specific.should_not include('eval-script')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "#supports_standard?" do
|
106
|
+
['IE', 'Safari', 'Chrome'].each do |browser_name|
|
107
|
+
it "returns true for #{browser_name}" do
|
108
|
+
browser = Brwsr::Browser.new(:ua => browser_name)
|
109
|
+
subject.stub(:browser).and_return(browser)
|
110
|
+
subject.send(:supports_standard?).should be_true
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it "returns true for Firefox v >= 18" do
|
115
|
+
browser = Brwsr::Browser.new(:ua => "Firefox 18")
|
116
|
+
subject.stub(:browser).and_return(browser)
|
117
|
+
subject.send(:supports_standard?).should be_true
|
118
|
+
end
|
119
|
+
|
120
|
+
it "returns false for Firefox v < 18" do
|
121
|
+
browser = Brwsr::Browser.new(:ua => "Firefox 17")
|
122
|
+
subject.stub(:browser).and_return(browser)
|
123
|
+
subject.send(:supports_standard?).should be_false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "#same_origin?" do
|
128
|
+
let(:origin) {"https://example.com:123"}
|
129
|
+
|
130
|
+
it "matches when host, scheme, and port match" do
|
131
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "https://example.com"), {:report_uri => 'https://example.com'})
|
132
|
+
csp.send(:same_origin?).should be_true
|
133
|
+
|
134
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "https://example.com:443"), {:report_uri => 'https://example.com'})
|
135
|
+
csp.send(:same_origin?).should be_true
|
136
|
+
|
137
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "https://example.com:123"), {:report_uri => 'https://example.com:123'})
|
138
|
+
csp.send(:same_origin?).should be_true
|
139
|
+
|
140
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com"), {:report_uri => 'http://example.com'})
|
141
|
+
csp.send(:same_origin?).should be_true
|
142
|
+
|
143
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com"), {:report_uri => 'http://example.com:80'})
|
144
|
+
csp.send(:same_origin?).should be_true
|
145
|
+
|
146
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com:80"), {:report_uri => 'http://example.com'})
|
147
|
+
csp.send(:same_origin?).should be_true
|
148
|
+
end
|
149
|
+
|
150
|
+
it "does not match port mismatches" do
|
151
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com:81"), {:report_uri => 'http://example.com'})
|
152
|
+
csp.send(:same_origin?).should be_false
|
153
|
+
end
|
154
|
+
|
155
|
+
it "does not match host mismatches" do
|
156
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com"), {:report_uri => 'http://twitter.com'})
|
157
|
+
csp.send(:same_origin?).should be_false
|
158
|
+
end
|
159
|
+
|
160
|
+
it "does not match host mismatches because of subdomains" do
|
161
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://sub.example.com"), {:report_uri => 'http://example.com'})
|
162
|
+
csp.send(:same_origin?).should be_false
|
163
|
+
end
|
164
|
+
|
165
|
+
it "does not match scheme mismatches" do
|
166
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "ftp://example.com"), {:report_uri => 'https://example.com'})
|
167
|
+
csp.send(:same_origin?).should be_false
|
168
|
+
end
|
169
|
+
|
170
|
+
it "does not match on substring collisions" do
|
171
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, "https://anotherexample.com"), {:report_uri => 'https://example.com'})
|
172
|
+
csp.send(:same_origin?).should be_false
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "#normalize_reporting_endpoint" do
|
177
|
+
let(:opts) {{:report_uri => 'https://example.com/csp', :forward_endpoint => anything}}
|
178
|
+
|
179
|
+
context "when using firefox" do
|
180
|
+
it "updates the report-uri when posting to a different host" do
|
181
|
+
subject.configure(request_for(FIREFOX, "https://anexample.com"), opts)
|
182
|
+
subject.report_uri.should == FF_CSP_ENDPOINT
|
183
|
+
end
|
184
|
+
|
185
|
+
it "updates the report-uri when posting to a different host for Firefox >= 18" do
|
186
|
+
subject.configure(request_for(FIREFOX, "https://anexample.com"), opts)
|
187
|
+
subject.report_uri.should == FF_CSP_ENDPOINT
|
188
|
+
end
|
189
|
+
|
190
|
+
it "doesn't set report-uri if no forward_endpoint is supplied" do
|
191
|
+
subject.configure(request_for(FIREFOX, "https://example.com"), :report_uri => "https://another.example.com")
|
192
|
+
subject.report_uri.should be_nil
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
it "does not update the URI is the report_uri is on the same origin" do
|
197
|
+
opts = {:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com'}
|
198
|
+
subject.configure(request_for(FIREFOX, "https://example.com/somewhere"), opts)
|
199
|
+
subject.report_uri.should == 'https://example.com/csp'
|
200
|
+
end
|
201
|
+
|
202
|
+
it "does not update the report-uri when using a non-firefox browser" do
|
203
|
+
subject.configure(request_for(CHROME), opts)
|
204
|
+
subject.report_uri.should == 'https://example.com/csp'
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe "#value" do
|
209
|
+
it "raises an exception when default-src is missing" do
|
210
|
+
subject.configure(request_for(CHROME), {:script_src => 'anything'})
|
211
|
+
lambda {
|
212
|
+
subject.value
|
213
|
+
}.should raise_error(ContentSecurityPolicyBuildError, "Couldn't build CSP header :( Expected to find default_src directive value")
|
214
|
+
end
|
215
|
+
|
216
|
+
context "auto-whitelists data: uris for img-src" do
|
217
|
+
|
218
|
+
it "sets the value if no img-src specified" do
|
219
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME), {:default_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true})
|
220
|
+
csp.value.should == "default-src 'self'; img-src data:;"
|
221
|
+
end
|
222
|
+
|
223
|
+
it "appends the value if img-src is specified" do
|
224
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME), {:default_src => 'self', :img_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true})
|
225
|
+
csp.value.should == "default-src 'self'; img-src 'self' data:;"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
it "fills in directives without values with default-src value" do
|
230
|
+
options = default_opts.merge(:disable_fill_missing => false)
|
231
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME), options)
|
232
|
+
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://* chrome-extension: about:; report-uri /csp_report;"
|
233
|
+
csp.value.should == value
|
234
|
+
end
|
235
|
+
|
236
|
+
it "sends the chrome csp header if an unknown browser is supplied" do
|
237
|
+
csp = ContentSecurityPolicy.new(request_for(IE), default_opts)
|
238
|
+
csp.value.should match "default-src"
|
239
|
+
end
|
240
|
+
|
241
|
+
context "X-Content-Security-Policy" do
|
242
|
+
it "builds a csp header for firefox" do
|
243
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX), default_opts)
|
244
|
+
csp.value.should == "allow https://*; options inline-script eval-script; img-src data:; script-src https://* data:; style-src https://* chrome-extension: about:;"
|
245
|
+
end
|
246
|
+
|
247
|
+
it "copies connect-src values to xhr_src values" do
|
248
|
+
opts = {
|
249
|
+
:default_src => 'http://twitter.com',
|
250
|
+
:connect_src => 'self http://*.localhost.com:*',
|
251
|
+
:disable_chrome_extension => true,
|
252
|
+
:disable_fill_missing => true
|
253
|
+
}
|
254
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX), opts)
|
255
|
+
csp.value.should =~ /xhr-src 'self' http:/
|
256
|
+
end
|
257
|
+
|
258
|
+
it "copies connect-src values to xhr_src values for FF 18" do
|
259
|
+
opts = {
|
260
|
+
:default_src => 'http://twitter.com',
|
261
|
+
:connect_src => 'self http://*.localhost.com:*',
|
262
|
+
:disable_chrome_extension => true,
|
263
|
+
:disable_fill_missing => true
|
264
|
+
}
|
265
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX_18), opts)
|
266
|
+
csp.value.should =~ /xhr-src 'self' http:\/\/\*\.localhost\.com:\*/
|
267
|
+
end
|
268
|
+
|
269
|
+
it "builds a w3c-style-ish header for Firefox > version 18" do
|
270
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX_18), default_opts)
|
271
|
+
csp.value.should =~ /default-src/
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
context "X-Webkit-CSP" do
|
276
|
+
it "builds a csp header for chrome" do
|
277
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME), default_opts)
|
278
|
+
csp.value.should == "default-src https://*; img-src data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* chrome-extension: about:; report-uri /csp_report;"
|
279
|
+
end
|
280
|
+
|
281
|
+
it "ignores :forward_endpoint settings" do
|
282
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME), @options_with_forwarding)
|
283
|
+
csp.value.should =~ /report-uri #{@options_with_forwarding[:report_uri]};/
|
284
|
+
end
|
285
|
+
|
286
|
+
it "whitelists chrome_extensions by default" do
|
287
|
+
opts = {
|
288
|
+
:default_src => 'https://*',
|
289
|
+
:report_uri => '/csp_report',
|
290
|
+
:script_src => 'inline eval https://* data:',
|
291
|
+
:style_src => "inline https://* chrome-extension: about:"
|
292
|
+
}
|
293
|
+
|
294
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME), opts)
|
295
|
+
|
296
|
+
# ignore the report-uri directive
|
297
|
+
csp.value.split(';')[0...-1].each{|directive| directive.should =~ /chrome-extension:/}
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
context "when supplying a experimental values" do
|
302
|
+
let(:options) {{
|
303
|
+
:disable_chrome_extension => true,
|
304
|
+
:disable_fill_missing => true,
|
305
|
+
:default_src => 'self',
|
306
|
+
:script_src => 'https://*',
|
307
|
+
:experimental => {
|
308
|
+
:script_src => 'self'
|
309
|
+
}
|
310
|
+
}}
|
311
|
+
|
312
|
+
let(:header) {}
|
313
|
+
it "returns the original value" do
|
314
|
+
header = ContentSecurityPolicy.new(request_for(CHROME), options)
|
315
|
+
header.value.should == "default-src 'self'; img-src data:; script-src https://*;"
|
316
|
+
end
|
317
|
+
|
318
|
+
it "it returns the experimental value if requested" do
|
319
|
+
header = ContentSecurityPolicy.new(request_for(CHROME), options, :experimental => true)
|
320
|
+
header.value.should_not =~ /https/
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
context "when supplying additional http directive values" do
|
325
|
+
let(:options) {
|
326
|
+
default_opts.merge({
|
327
|
+
:http_additions => {
|
328
|
+
:frame_src => "http://*",
|
329
|
+
:img_src => "http://*"
|
330
|
+
}
|
331
|
+
})
|
332
|
+
}
|
333
|
+
|
334
|
+
it "adds directive values for headers on http" do
|
335
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME), options)
|
336
|
+
csp.value.should == "default-src https://*; frame-src http://*; img-src http://* data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* chrome-extension: about:; report-uri /csp_report;"
|
337
|
+
end
|
338
|
+
|
339
|
+
it "does not add the directive values if requesting https" do
|
340
|
+
csp = ContentSecurityPolicy.new(request_for(CHROME, '/', :ssl => true), options)
|
341
|
+
csp.value.should_not =~ /http:/
|
342
|
+
end
|
343
|
+
|
344
|
+
context "when supplying an experimental block" do
|
345
|
+
# this simulates the situation where we are enforcing that scripts
|
346
|
+
# only come from http[s]? depending if we're on ssl or not. The
|
347
|
+
# report only tag will allow scripts from self over ssl, and
|
348
|
+
# from a secure CDN over non-ssl
|
349
|
+
let(:options) {{
|
350
|
+
:disable_chrome_extension => true,
|
351
|
+
:disable_fill_missing => true,
|
352
|
+
:default_src => 'self',
|
353
|
+
:script_src => 'https://*',
|
354
|
+
:http_additions => {
|
355
|
+
:script_src => 'http://*'
|
356
|
+
},
|
357
|
+
:experimental => {
|
358
|
+
:script_src => 'self',
|
359
|
+
:http_additions => {
|
360
|
+
:script_src => 'https://mycdn.example.com'
|
361
|
+
}
|
362
|
+
}
|
363
|
+
}}
|
364
|
+
# for comparison purposes, if not using the experimental header this would produce
|
365
|
+
# "allow 'self'; script-src https://*" for https requests
|
366
|
+
# and
|
367
|
+
# "allow 'self; script-src https://* http://*" for http requests
|
368
|
+
|
369
|
+
it "uses the value in the experimental block over SSL" do
|
370
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX, '/', :ssl => true), options, :experimental => true)
|
371
|
+
csp.value.should == "allow 'self'; img-src data:; script-src 'self';"
|
372
|
+
end
|
373
|
+
|
374
|
+
it "merges the values from experimental/http_additions when not over SSL" do
|
375
|
+
csp = ContentSecurityPolicy.new(request_for(FIREFOX), options, :experimental => true)
|
376
|
+
csp.value.should == "allow 'self'; img-src data:; script-src 'self' https://mycdn.example.com;"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|