secure_headers 1.1.1 → 1.4.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.
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -3
- data/Gemfile +2 -7
- data/HISTORY.md +47 -0
- data/README.md +74 -27
- data/app/controllers/content_security_policy_controller.rb +1 -0
- data/fixtures/rails_3_2_12/Gemfile +0 -8
- data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -1
- data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +0 -3
- data/fixtures/rails_3_2_12/app/views/things/index.html.erb +1 -21
- data/fixtures/rails_3_2_12/config/application.rb +0 -54
- data/fixtures/rails_3_2_12/config/environments/test.rb +2 -2
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -0
- data/fixtures/rails_3_2_12/config/routes.rb +0 -57
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +15 -14
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +12 -12
- data/fixtures/rails_3_2_12/spec/spec_helper.rb +1 -5
- data/fixtures/rails_3_2_12_no_init/Gemfile +0 -9
- data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
- data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
- data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
- data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -51
- data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +2 -2
- data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +12 -12
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +12 -12
- data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -18
- data/lib/secure_headers/headers/content_security_policy.rb +54 -35
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_download_options.rb +39 -0
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/padrino.rb +14 -0
- data/lib/secure_headers/railtie.rb +6 -2
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers.rb +9 -2
- data/spec/controllers/content_security_policy_controller_spec.rb +19 -19
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +117 -85
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +21 -20
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +32 -0
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +20 -19
- data/spec/lib/secure_headers_spec.rb +38 -12
- data/spec/spec_helper.rb +10 -24
- metadata +9 -37
- data/.rvmrc +0 -1
- data/Guardfile +0 -10
- data/fixtures/rails_3_2_12/Guardfile +0 -14
- data/fixtures/rails_3_2_12/app/models/thing.rb +0 -3
- data/fixtures/rails_3_2_12/config/database.yml +0 -25
- data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
- data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
- data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
- data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
- data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
- data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
- data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
- data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
- data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
- data/fixtures/rails_3_2_12/db/schema.rb +0 -16
- data/fixtures/rails_3_2_12/db/seeds.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/Guardfile +0 -14
- data/fixtures/rails_3_2_12_no_init/app/models/thing.rb +0 -3
- data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
- data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
- data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
- data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
- data/fixtures/rails_3_2_12_no_init/config/database.yml +0 -25
- data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
- data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
- data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
- data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
- data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
- data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
- data/fixtures/rails_3_2_12_no_init/db/schema.rb +0 -16
- data/fixtures/rails_3_2_12_no_init/db/seeds.rb +0 -7
|
@@ -32,58 +32,58 @@ describe ContentSecurityPolicyController do
|
|
|
32
32
|
let(:secondary_endpoint) { "https://internal.example.com" }
|
|
33
33
|
|
|
34
34
|
before(:each) do
|
|
35
|
-
SecureHeaders::Configuration.
|
|
36
|
-
subject.
|
|
37
|
-
subject.
|
|
38
|
-
subject.
|
|
39
|
-
Net::HTTP.
|
|
35
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_return({:report_uri => endpoint, :forward_endpoint => secondary_endpoint})
|
|
36
|
+
expect(subject).to receive :head
|
|
37
|
+
allow(subject).to receive(:params).and_return(params)
|
|
38
|
+
allow(subject).to receive(:request).and_return(FakeRequest.new)
|
|
39
|
+
allow_any_instance_of(Net::HTTP).to receive(:request)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
context "delivery endpoint" do
|
|
43
43
|
it "posts over ssl" do
|
|
44
|
-
subject.
|
|
44
|
+
expect(subject).to receive(:use_ssl)
|
|
45
45
|
subject.scribe
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
it "posts over plain http" do
|
|
49
|
-
SecureHeaders::Configuration.
|
|
50
|
-
subject.
|
|
49
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_return(:report_uri => 'http://example.com')
|
|
50
|
+
expect(subject).not_to receive(:use_ssl)
|
|
51
51
|
subject.scribe
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
it "makes a POST request" do
|
|
56
|
-
Net::HTTP.
|
|
57
|
-
request.
|
|
58
|
-
params.
|
|
56
|
+
allow(Net::HTTP).to receive(:new).and_return(request)
|
|
57
|
+
expect(request).to receive(:request).with(instance_of(::Net::HTTP::Post))
|
|
58
|
+
allow(params).to receive(:to_json)
|
|
59
59
|
subject.scribe
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
it "POSTs to the configured forward_endpoint" do
|
|
63
|
-
Net::HTTP::Post.
|
|
63
|
+
expect(Net::HTTP::Post).to receive(:new).with(secondary_endpoint).and_return(request)
|
|
64
64
|
subject.scribe
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
it "does not POST if there is no forwarder configured" do
|
|
68
|
-
SecureHeaders::Configuration.
|
|
69
|
-
Net::HTTP::Post.
|
|
68
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_return({})
|
|
69
|
+
expect(Net::HTTP::Post).not_to receive(:new)
|
|
70
70
|
subject.scribe
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
it "eliminates known phony CSP reports" do
|
|
74
|
-
SecureHeaders::Configuration.
|
|
75
|
-
Net::HTTP::Post.
|
|
74
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_return(:report_uri => nil)
|
|
75
|
+
expect(Net::HTTP::Post).not_to receive :new
|
|
76
76
|
subject.scribe
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
it "logs errors when it cannot forward the CSP report" do
|
|
80
80
|
class Rails; def logger; end; end
|
|
81
81
|
logger = double(:repond_to? => true)
|
|
82
|
-
Rails.
|
|
82
|
+
allow(Rails).to receive(:logger).and_return(logger)
|
|
83
83
|
|
|
84
|
-
SecureHeaders::Configuration.
|
|
84
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_raise(StandardError)
|
|
85
85
|
|
|
86
|
-
logger.
|
|
86
|
+
expect(logger).to receive(:warn)
|
|
87
87
|
subject.scribe
|
|
88
88
|
end
|
|
89
89
|
end
|
|
@@ -30,38 +30,38 @@ module SecureHeaders
|
|
|
30
30
|
|
|
31
31
|
describe "#name" do
|
|
32
32
|
context "when supplying options to override request" do
|
|
33
|
-
specify { ContentSecurityPolicy.new(default_opts, :ua => IE).name.
|
|
34
|
-
specify { ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name.
|
|
35
|
-
specify { ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name.
|
|
36
|
-
specify { ContentSecurityPolicy.new(default_opts, :ua => CHROME).name.
|
|
37
|
-
specify { ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name.
|
|
33
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
34
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
35
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
36
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
37
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
context "when in report-only mode" do
|
|
41
|
-
specify { ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name.
|
|
42
|
-
specify { ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name.
|
|
43
|
-
specify { ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name.
|
|
44
|
-
specify { ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name.
|
|
45
|
-
specify { ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name.
|
|
41
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
42
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
43
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
44
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
45
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
context "when in enforce mode" do
|
|
49
49
|
let(:opts) { default_opts.merge(:enforce => true)}
|
|
50
50
|
|
|
51
|
-
specify { ContentSecurityPolicy.new(opts, :request => request_for(IE)).name.
|
|
52
|
-
specify { ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name.
|
|
53
|
-
specify { ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name.
|
|
54
|
-
specify { ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name.
|
|
55
|
-
specify { ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name.
|
|
51
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(STANDARD_HEADER_NAME)}
|
|
52
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(STANDARD_HEADER_NAME)}
|
|
53
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(STANDARD_HEADER_NAME)}
|
|
54
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(STANDARD_HEADER_NAME)}
|
|
55
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(STANDARD_HEADER_NAME)}
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
context "when in experimental mode" do
|
|
59
59
|
let(:opts) { default_opts.merge(:enforce => true).merge(:experimental => {})}
|
|
60
|
-
specify { ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(IE)}).name.
|
|
61
|
-
specify { ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX)}).name.
|
|
62
|
-
specify { ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX_23)}).name.
|
|
63
|
-
specify { ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME)}).name.
|
|
64
|
-
specify { ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME_25)}).name.
|
|
60
|
+
specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(IE)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
61
|
+
specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
62
|
+
specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX_23)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
63
|
+
specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
64
|
+
specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME_25)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -76,7 +76,41 @@ module SecureHeaders
|
|
|
76
76
|
context "Content-Security-Policy" do
|
|
77
77
|
it "converts the script values to their equivilents" do
|
|
78
78
|
csp = ContentSecurityPolicy.new(@opts, :request => request_for(CHROME))
|
|
79
|
-
csp.value.
|
|
79
|
+
expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https://* data: 'self' 'none'")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "adds a @enforce and @app_name variables to the report uri" do
|
|
83
|
+
opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => 'twitter')
|
|
84
|
+
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
85
|
+
expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "does not add an empty @app_name variable to the report uri" do
|
|
89
|
+
opts = @opts.merge(:tag_report_uri => true, :enforce => true)
|
|
90
|
+
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
91
|
+
expect(csp.value).to include("/csp_report?enforce=true")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "accepts procs for report-uris" do
|
|
95
|
+
opts = {
|
|
96
|
+
:default_src => 'self',
|
|
97
|
+
:report_uri => lambda { "http://lambda/result" }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
csp = ContentSecurityPolicy.new(opts)
|
|
101
|
+
expect(csp.report_uri).to eq("http://lambda/result")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "accepts procs for other fields" do
|
|
105
|
+
opts = {
|
|
106
|
+
:default_src => lambda { "http://lambda/result" },
|
|
107
|
+
:enforce => lambda { true },
|
|
108
|
+
:disable_fill_missing => lambda { true }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
csp = ContentSecurityPolicy.new(opts)
|
|
112
|
+
expect(csp.value).to eq("default-src http://lambda/result; img-src http://lambda/result data:;")
|
|
113
|
+
expect(csp.name).to match("Content-Security-Policy")
|
|
80
114
|
end
|
|
81
115
|
end
|
|
82
116
|
end
|
|
@@ -86,47 +120,47 @@ module SecureHeaders
|
|
|
86
120
|
|
|
87
121
|
it "matches when host, scheme, and port match" do
|
|
88
122
|
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com"))
|
|
89
|
-
csp.send(:same_origin?).
|
|
123
|
+
expect(csp.send(:same_origin?)).to be true
|
|
90
124
|
|
|
91
125
|
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com:443"))
|
|
92
|
-
csp.send(:same_origin?).
|
|
126
|
+
expect(csp.send(:same_origin?)).to be true
|
|
93
127
|
|
|
94
128
|
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com:123'}, :request => request_for(FIREFOX, "https://example.com:123"))
|
|
95
|
-
csp.send(:same_origin?).
|
|
129
|
+
expect(csp.send(:same_origin?)).to be true
|
|
96
130
|
|
|
97
131
|
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
98
|
-
csp.send(:same_origin?).
|
|
132
|
+
expect(csp.send(:same_origin?)).to be true
|
|
99
133
|
|
|
100
134
|
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com:80'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
101
|
-
csp.send(:same_origin?).
|
|
135
|
+
expect(csp.send(:same_origin?)).to be true
|
|
102
136
|
|
|
103
137
|
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:80"))
|
|
104
|
-
csp.send(:same_origin?).
|
|
138
|
+
expect(csp.send(:same_origin?)).to be true
|
|
105
139
|
end
|
|
106
140
|
|
|
107
141
|
it "does not match port mismatches" do
|
|
108
142
|
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:81"))
|
|
109
|
-
csp.send(:same_origin?).
|
|
143
|
+
expect(csp.send(:same_origin?)).to be false
|
|
110
144
|
end
|
|
111
145
|
|
|
112
146
|
it "does not match host mismatches" do
|
|
113
147
|
csp = ContentSecurityPolicy.new({:report_uri => 'http://twitter.com'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
114
|
-
csp.send(:same_origin?).
|
|
148
|
+
expect(csp.send(:same_origin?)).to be false
|
|
115
149
|
end
|
|
116
150
|
|
|
117
151
|
it "does not match host mismatches because of subdomains" do
|
|
118
152
|
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://sub.example.com"))
|
|
119
|
-
csp.send(:same_origin?).
|
|
153
|
+
expect(csp.send(:same_origin?)).to be false
|
|
120
154
|
end
|
|
121
155
|
|
|
122
156
|
it "does not match scheme mismatches" do
|
|
123
157
|
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "ftp://example.com"))
|
|
124
|
-
csp.send(:same_origin?).
|
|
158
|
+
expect(csp.send(:same_origin?)).to be false
|
|
125
159
|
end
|
|
126
160
|
|
|
127
161
|
it "does not match on substring collisions" do
|
|
128
162
|
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://anotherexample.com"))
|
|
129
|
-
csp.send(:same_origin?).
|
|
163
|
+
expect(csp.send(:same_origin?)).to be false
|
|
130
164
|
end
|
|
131
165
|
end
|
|
132
166
|
|
|
@@ -136,29 +170,29 @@ module SecureHeaders
|
|
|
136
170
|
context "when using firefox" do
|
|
137
171
|
it "updates the report-uri when posting to a different host" do
|
|
138
172
|
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://anexample.com"))
|
|
139
|
-
csp.report_uri.
|
|
173
|
+
expect(csp.report_uri).to eq(FF_CSP_ENDPOINT)
|
|
140
174
|
end
|
|
141
175
|
|
|
142
176
|
it "doesn't change report-uri if a path supplied" do
|
|
143
177
|
csp = ContentSecurityPolicy.new({:report_uri => "/csp_reports"}, :request => request_for(FIREFOX, "https://anexample.com"))
|
|
144
|
-
csp.report_uri.
|
|
178
|
+
expect(csp.report_uri).to eq("/csp_reports")
|
|
145
179
|
end
|
|
146
180
|
|
|
147
181
|
it "forwards if the request_uri is set to a non-matching value" do
|
|
148
182
|
csp = ContentSecurityPolicy.new({:report_uri => "https://another.example.com", :forward_endpoint => '/somewhere'}, :ua => "Firefox", :request_uri => "https://anexample.com")
|
|
149
|
-
csp.report_uri.
|
|
183
|
+
expect(csp.report_uri).to eq(FF_CSP_ENDPOINT)
|
|
150
184
|
end
|
|
151
185
|
end
|
|
152
186
|
|
|
153
187
|
it "does not update the URI is the report_uri is on the same origin" do
|
|
154
188
|
opts = {:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com'}
|
|
155
189
|
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://example.com/somewhere"))
|
|
156
|
-
csp.report_uri.
|
|
190
|
+
expect(csp.report_uri).to eq('https://example.com/csp')
|
|
157
191
|
end
|
|
158
192
|
|
|
159
193
|
it "does not update the report-uri when using a non-firefox browser" do
|
|
160
194
|
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
161
|
-
csp.report_uri.
|
|
195
|
+
expect(csp.report_uri).to eq('https://example.com/csp')
|
|
162
196
|
end
|
|
163
197
|
|
|
164
198
|
context "when using a protocol-relative value for report-uri" do
|
|
@@ -171,20 +205,20 @@ module SecureHeaders
|
|
|
171
205
|
|
|
172
206
|
it "uses the current protocol" do
|
|
173
207
|
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, '/', :ssl => true))
|
|
174
|
-
csp.value.
|
|
208
|
+
expect(csp.value).to match(%r{report-uri https://example.com/csp;})
|
|
175
209
|
|
|
176
210
|
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX))
|
|
177
|
-
csp.value.
|
|
211
|
+
expect(csp.value).to match(%r{report-uri http://example.com/csp;})
|
|
178
212
|
end
|
|
179
213
|
|
|
180
214
|
it "uses the pre-configured https protocol" do
|
|
181
215
|
csp = ContentSecurityPolicy.new(opts, :ua => "Firefox", :ssl => true)
|
|
182
|
-
csp.value.
|
|
216
|
+
expect(csp.value).to match(%r{report-uri https://example.com/csp;})
|
|
183
217
|
end
|
|
184
218
|
|
|
185
219
|
it "uses the pre-configured http protocol" do
|
|
186
220
|
csp = ContentSecurityPolicy.new(opts, :ua => "Firefox", :ssl => false)
|
|
187
|
-
csp.value.
|
|
221
|
+
expect(csp.value).to match(%r{report-uri http://example.com/csp;})
|
|
188
222
|
end
|
|
189
223
|
end
|
|
190
224
|
end
|
|
@@ -192,20 +226,20 @@ module SecureHeaders
|
|
|
192
226
|
describe "#value" do
|
|
193
227
|
it "raises an exception when default-src is missing" do
|
|
194
228
|
csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME))
|
|
195
|
-
|
|
229
|
+
expect {
|
|
196
230
|
csp.value
|
|
197
|
-
}.
|
|
231
|
+
}.to raise_error(ContentSecurityPolicyBuildError, "Couldn't build CSP header :( Expected to find default_src directive value")
|
|
198
232
|
end
|
|
199
233
|
|
|
200
234
|
context "auto-whitelists data: uris for img-src" do
|
|
201
235
|
it "sets the value if no img-src specified" do
|
|
202
236
|
csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true}, :request => request_for(CHROME))
|
|
203
|
-
csp.value.
|
|
237
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
204
238
|
end
|
|
205
239
|
|
|
206
240
|
it "appends the value if img-src is specified" do
|
|
207
241
|
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true}, :request => request_for(CHROME))
|
|
208
|
-
csp.value.
|
|
242
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
209
243
|
end
|
|
210
244
|
end
|
|
211
245
|
|
|
@@ -213,30 +247,55 @@ module SecureHeaders
|
|
|
213
247
|
options = default_opts.merge(:disable_fill_missing => false)
|
|
214
248
|
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
215
249
|
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;"
|
|
216
|
-
csp.value.
|
|
250
|
+
expect(csp.value).to eq(value)
|
|
217
251
|
end
|
|
218
252
|
|
|
219
253
|
it "sends the standard csp header if an unknown browser is supplied" do
|
|
220
254
|
csp = ContentSecurityPolicy.new(default_opts, :request => request_for(IE))
|
|
221
|
-
csp.value.
|
|
255
|
+
expect(csp.value).to match "default-src"
|
|
222
256
|
end
|
|
223
257
|
|
|
224
258
|
context "Firefox" do
|
|
225
259
|
it "builds a csp header for firefox" do
|
|
226
260
|
csp = ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX))
|
|
227
|
-
csp.value.
|
|
261
|
+
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;")
|
|
228
262
|
end
|
|
229
263
|
end
|
|
230
264
|
|
|
231
265
|
context "Chrome" do
|
|
232
266
|
it "builds a csp header for chrome" do
|
|
233
267
|
csp = ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME))
|
|
234
|
-
csp.value.
|
|
268
|
+
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;")
|
|
235
269
|
end
|
|
236
270
|
|
|
237
271
|
it "ignores :forward_endpoint settings" do
|
|
238
272
|
csp = ContentSecurityPolicy.new(@options_with_forwarding, :request => request_for(CHROME))
|
|
239
|
-
csp.value.
|
|
273
|
+
expect(csp.value).to match(/report-uri #{@options_with_forwarding[:report_uri]};/)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
context "when using a nonce" do
|
|
278
|
+
it "adds a nonce and unsafe-inline to the script-src value" do
|
|
279
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME))
|
|
280
|
+
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it "adds a nonce and unsafe-inline to the style-src value" do
|
|
284
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME))
|
|
285
|
+
expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it "adds an identical nonce to the style and script-src directives" do
|
|
289
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME))
|
|
290
|
+
nonce = header.nonce
|
|
291
|
+
value = header.value
|
|
292
|
+
expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
|
293
|
+
expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it "does not add 'unsafe-inline' twice" do
|
|
297
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME))
|
|
298
|
+
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';")
|
|
240
299
|
end
|
|
241
300
|
end
|
|
242
301
|
|
|
@@ -253,12 +312,12 @@ module SecureHeaders
|
|
|
253
312
|
|
|
254
313
|
it "returns the original value" do
|
|
255
314
|
header = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
256
|
-
header.value.
|
|
315
|
+
expect(header.value).to eq("default-src 'self'; img-src 'self' data:; script-src https://*;")
|
|
257
316
|
end
|
|
258
317
|
|
|
259
318
|
it "it returns the experimental value if requested" do
|
|
260
319
|
header = ContentSecurityPolicy.new(options, {:request => request_for(CHROME), :experimental => true})
|
|
261
|
-
header.value.
|
|
320
|
+
expect(header.value).not_to match(/https/)
|
|
262
321
|
end
|
|
263
322
|
end
|
|
264
323
|
|
|
@@ -274,17 +333,17 @@ module SecureHeaders
|
|
|
274
333
|
|
|
275
334
|
it "adds directive values for headers on http" do
|
|
276
335
|
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
277
|
-
csp.value.
|
|
336
|
+
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;")
|
|
278
337
|
end
|
|
279
338
|
|
|
280
339
|
it "does not add the directive values if requesting https" do
|
|
281
340
|
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME, '/', :ssl => true))
|
|
282
|
-
csp.value.
|
|
341
|
+
expect(csp.value).not_to match(/http:/)
|
|
283
342
|
end
|
|
284
343
|
|
|
285
344
|
it "does not add the directive values if requesting https" do
|
|
286
345
|
csp = ContentSecurityPolicy.new(options, :ua => "Chrome", :ssl => true)
|
|
287
|
-
csp.value.
|
|
346
|
+
expect(csp.value).not_to match(/http:/)
|
|
288
347
|
end
|
|
289
348
|
|
|
290
349
|
context "when supplying an experimental block" do
|
|
@@ -314,47 +373,20 @@ module SecureHeaders
|
|
|
314
373
|
|
|
315
374
|
it "uses the value in the experimental block over SSL" do
|
|
316
375
|
csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX, '/', :ssl => true))
|
|
317
|
-
csp.value.
|
|
376
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:; script-src 'self';")
|
|
318
377
|
end
|
|
319
378
|
|
|
320
379
|
it "detects the :ssl => true option" do
|
|
321
380
|
csp = ContentSecurityPolicy.new(options, :experimental => true, :ua => FIREFOX, :ssl => true)
|
|
322
|
-
csp.value.
|
|
381
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:; script-src 'self';")
|
|
323
382
|
end
|
|
324
383
|
|
|
325
384
|
it "merges the values from experimental/http_additions when not over SSL" do
|
|
326
385
|
csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX))
|
|
327
|
-
csp.value.
|
|
386
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:; script-src 'self' https://mycdn.example.com;")
|
|
328
387
|
end
|
|
329
388
|
end
|
|
330
389
|
end
|
|
331
|
-
|
|
332
|
-
context "when supplying a script nonce callback" do
|
|
333
|
-
let(:options) {
|
|
334
|
-
default_opts.merge({
|
|
335
|
-
:script_nonce => "random",
|
|
336
|
-
})
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
it "uses the value in the X-Webkit-CSP" do
|
|
340
|
-
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
341
|
-
csp.value.should match "script-nonce random;"
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
it "runs a dynamic nonce generator" do
|
|
345
|
-
options[:script_nonce] = lambda { 'something' }
|
|
346
|
-
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
347
|
-
csp.value.should match "script-nonce something;"
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
it "runs against the given controller context" do
|
|
351
|
-
fake_params = {}
|
|
352
|
-
options[:script_nonce] = lambda { params[:script_nonce] = 'something' }
|
|
353
|
-
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME), :controller => double(:params => fake_params))
|
|
354
|
-
csp.value.should match "script-nonce something;"
|
|
355
|
-
fake_params.should == {:script_nonce => 'something'}
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
390
|
end
|
|
359
391
|
end
|
|
360
392
|
end
|
|
@@ -2,59 +2,60 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe StrictTransportSecurity do
|
|
5
|
-
specify{ StrictTransportSecurity.new.name.
|
|
5
|
+
specify{ expect(StrictTransportSecurity.new.name).to eq("Strict-Transport-Security") }
|
|
6
6
|
|
|
7
7
|
describe "#value" do
|
|
8
|
-
specify { StrictTransportSecurity.new.value.
|
|
9
|
-
specify { StrictTransportSecurity.new("max-age=1234").value.
|
|
10
|
-
specify { StrictTransportSecurity.new(:max_age => '1234').value.
|
|
11
|
-
specify { StrictTransportSecurity.new(:max_age => 1234).value.
|
|
12
|
-
specify { StrictTransportSecurity.new(:max_age => HSTS_MAX_AGE, :include_subdomains => true).value.
|
|
8
|
+
specify { expect(StrictTransportSecurity.new.value).to eq(StrictTransportSecurity::Constants::DEFAULT_VALUE)}
|
|
9
|
+
specify { expect(StrictTransportSecurity.new("max-age=1234").value).to eq("max-age=1234")}
|
|
10
|
+
specify { expect(StrictTransportSecurity.new(:max_age => '1234').value).to eq("max-age=1234")}
|
|
11
|
+
specify { expect(StrictTransportSecurity.new(:max_age => 1234).value).to eq("max-age=1234")}
|
|
12
|
+
specify { expect(StrictTransportSecurity.new(:max_age => HSTS_MAX_AGE, :include_subdomains => true).value).to eq("max-age=#{HSTS_MAX_AGE}; includeSubdomains")}
|
|
13
|
+
specify { expect(StrictTransportSecurity.new(:max_age => HSTS_MAX_AGE, :include_subdomains => true, :preload => true).value).to eq("max-age=#{HSTS_MAX_AGE}; includeSubdomains; preload")}
|
|
13
14
|
|
|
14
15
|
context "with an invalid configuration" do
|
|
15
16
|
context "with a hash argument" do
|
|
16
17
|
it "should allow string values for max-age" do
|
|
17
|
-
|
|
18
|
+
expect {
|
|
18
19
|
StrictTransportSecurity.new(:max_age => '1234')
|
|
19
|
-
}.
|
|
20
|
+
}.not_to raise_error
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
it "should allow integer values for max-age" do
|
|
23
|
-
|
|
24
|
+
expect {
|
|
24
25
|
StrictTransportSecurity.new(:max_age => 1234)
|
|
25
|
-
}.
|
|
26
|
+
}.not_to raise_error
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
it "raises an exception with an invalid max-age" do
|
|
29
|
-
|
|
30
|
+
expect {
|
|
30
31
|
StrictTransportSecurity.new(:max_age => 'abc123')
|
|
31
|
-
}.
|
|
32
|
+
}.to raise_error(STSBuildError)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
it "raises an exception if max-age is not supplied" do
|
|
35
|
-
|
|
36
|
+
expect {
|
|
36
37
|
StrictTransportSecurity.new(:includeSubdomains => true)
|
|
37
|
-
}.
|
|
38
|
+
}.to raise_error(STSBuildError)
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
context "with a string argument" do
|
|
42
43
|
it "raises an exception with an invalid max-age" do
|
|
43
|
-
|
|
44
|
+
expect {
|
|
44
45
|
StrictTransportSecurity.new('max-age=abc123')
|
|
45
|
-
}.
|
|
46
|
+
}.to raise_error(STSBuildError)
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
it "raises an exception if max-age is not supplied" do
|
|
49
|
-
|
|
50
|
+
expect {
|
|
50
51
|
StrictTransportSecurity.new('includeSubdomains')
|
|
51
|
-
}.
|
|
52
|
+
}.to raise_error(STSBuildError)
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
it "raises an exception with an invalid format" do
|
|
55
|
-
|
|
56
|
+
expect {
|
|
56
57
|
StrictTransportSecurity.new('max-age=123includeSubdomains')
|
|
57
|
-
}.
|
|
58
|
+
}.to raise_error(STSBuildError)
|
|
58
59
|
end
|
|
59
60
|
end
|
|
60
61
|
end
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
describe XContentTypeOptions do
|
|
3
|
-
specify{ XContentTypeOptions.new.name.
|
|
3
|
+
specify{ expect(XContentTypeOptions.new.name).to eq("X-Content-Type-Options") }
|
|
4
4
|
|
|
5
5
|
describe "#value" do
|
|
6
|
-
specify { XContentTypeOptions.new.value.
|
|
7
|
-
specify { XContentTypeOptions.new("nosniff").value.
|
|
8
|
-
specify { XContentTypeOptions.new(:value => 'nosniff').value.
|
|
6
|
+
specify { expect(XContentTypeOptions.new.value).to eq(XContentTypeOptions::Constants::DEFAULT_VALUE)}
|
|
7
|
+
specify { expect(XContentTypeOptions.new("nosniff").value).to eq("nosniff")}
|
|
8
|
+
specify { expect(XContentTypeOptions.new(:value => 'nosniff').value).to eq("nosniff")}
|
|
9
9
|
|
|
10
10
|
context "invalid configuration values" do
|
|
11
11
|
it "accepts nosniff" do
|
|
12
|
-
|
|
12
|
+
expect {
|
|
13
13
|
XContentTypeOptions.new("nosniff")
|
|
14
|
-
}.
|
|
14
|
+
}.not_to raise_error
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
expect {
|
|
17
17
|
XContentTypeOptions.new(:value => "nosniff")
|
|
18
|
-
}.
|
|
18
|
+
}.not_to raise_error
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
it "accepts nil" do
|
|
22
|
-
|
|
22
|
+
expect {
|
|
23
23
|
XContentTypeOptions.new
|
|
24
|
-
}.
|
|
24
|
+
}.not_to raise_error
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
it "doesn't accept anything besides no-sniff" do
|
|
28
|
-
|
|
28
|
+
expect {
|
|
29
29
|
XContentTypeOptions.new("donkey")
|
|
30
|
-
}.
|
|
30
|
+
}.to raise_error
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
describe XDownloadOptions do
|
|
3
|
+
specify { expect(XDownloadOptions.new.name).to eq(XDO_HEADER_NAME)}
|
|
4
|
+
specify { expect(XDownloadOptions.new.value).to eq("noopen")}
|
|
5
|
+
specify { expect(XDownloadOptions.new('noopen').value).to eq('noopen')}
|
|
6
|
+
specify { expect(XDownloadOptions.new(:value => 'noopen').value).to eq('noopen') }
|
|
7
|
+
|
|
8
|
+
context "invalid configuration values" do
|
|
9
|
+
it "accepts noopen" do
|
|
10
|
+
expect {
|
|
11
|
+
XDownloadOptions.new("noopen")
|
|
12
|
+
}.not_to raise_error
|
|
13
|
+
|
|
14
|
+
expect {
|
|
15
|
+
XDownloadOptions.new(:value => "noopen")
|
|
16
|
+
}.not_to raise_error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "accepts nil" do
|
|
20
|
+
expect {
|
|
21
|
+
XDownloadOptions.new
|
|
22
|
+
}.not_to raise_error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "doesn't accept anything besides noopen" do
|
|
26
|
+
expect {
|
|
27
|
+
XContentTypeOptions.new("open")
|
|
28
|
+
}.to raise_error
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|