secure_headers 2.3.0 → 3.0.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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +3 -0
- data/CHANGELOG.md +205 -0
- data/Gemfile +9 -10
- data/Guardfile +12 -0
- data/README.md +169 -301
- data/Rakefile +2 -36
- data/lib/secure_headers/configuration.rb +189 -0
- data/lib/secure_headers/headers/content_security_policy.rb +362 -213
- data/lib/secure_headers/headers/public_key_pins.rb +43 -58
- data/lib/secure_headers/headers/strict_transport_security.rb +21 -48
- data/lib/secure_headers/headers/x_content_type_options.rb +18 -32
- data/lib/secure_headers/headers/x_download_options.rb +18 -32
- data/lib/secure_headers/headers/x_frame_options.rb +24 -33
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -33
- data/lib/secure_headers/headers/x_xss_protection.rb +17 -47
- data/lib/secure_headers/middleware.rb +15 -0
- data/lib/secure_headers/padrino.rb +1 -2
- data/lib/secure_headers/railtie.rb +9 -6
- data/lib/secure_headers/view_helper.rb +27 -44
- data/lib/secure_headers.rb +229 -162
- data/secure_headers.gemspec +7 -11
- data/spec/lib/secure_headers/configuration_spec.rb +80 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +175 -262
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +17 -17
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +11 -43
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +13 -18
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +13 -17
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +15 -17
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +22 -39
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +22 -30
- data/spec/lib/secure_headers/middleware_spec.rb +40 -0
- data/spec/lib/secure_headers_spec.rb +202 -326
- data/spec/spec_helper.rb +32 -28
- data/upgrading-to-3-0.md +37 -0
- metadata +14 -94
- data/fixtures/rails_3_2_12/.rspec +0 -1
- data/fixtures/rails_3_2_12/Gemfile +0 -6
- data/fixtures/rails_3_2_12/README.rdoc +0 -261
- data/fixtures/rails_3_2_12/Rakefile +0 -7
- data/fixtures/rails_3_2_12/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_12/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_12/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +0 -11
- data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_3_2_12/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_3_2_12/config/application.rb +0 -14
- data/fixtures/rails_3_2_12/config/boot.rb +0 -6
- data/fixtures/rails_3_2_12/config/environment.rb +0 -5
- data/fixtures/rails_3_2_12/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +0 -17
- data/fixtures/rails_3_2_12/config/routes.rb +0 -4
- data/fixtures/rails_3_2_12/config/script_hashes.yml +0 -5
- data/fixtures/rails_3_2_12/config.ru +0 -7
- data/fixtures/rails_3_2_12/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +0 -83
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_12/spec/spec_helper.rb +0 -15
- data/fixtures/rails_3_2_12/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/.rspec +0 -1
- data/fixtures/rails_3_2_12_no_init/Gemfile +0 -5
- data/fixtures/rails_3_2_12_no_init/README.rdoc +0 -261
- data/fixtures/rails_3_2_12_no_init/Rakefile +0 -7
- data/fixtures/rails_3_2_12_no_init/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +0 -20
- data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -12
- data/fixtures/rails_3_2_12_no_init/app/views/other_things/index.html.erb +0 -1
- data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -0
- data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -17
- data/fixtures/rails_3_2_12_no_init/config/boot.rb +0 -6
- data/fixtures/rails_3_2_12_no_init/config/environment.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -4
- data/fixtures/rails_3_2_12_no_init/config.ru +0 -4
- data/fixtures/rails_3_2_12_no_init/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +0 -56
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_4_1_8/Gemfile +0 -5
- data/fixtures/rails_4_1_8/README.rdoc +0 -28
- data/fixtures/rails_4_1_8/Rakefile +0 -6
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_4_1_8/app/models/.keep +0 -0
- data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +0 -11
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_4_1_8/config/application.rb +0 -15
- data/fixtures/rails_4_1_8/config/boot.rb +0 -4
- data/fixtures/rails_4_1_8/config/environment.rb +0 -5
- data/fixtures/rails_4_1_8/config/environments/test.rb +0 -10
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +0 -17
- data/fixtures/rails_4_1_8/config/routes.rb +0 -4
- data/fixtures/rails_4_1_8/config/script_hashes.yml +0 -5
- data/fixtures/rails_4_1_8/config/secrets.yml +0 -22
- data/fixtures/rails_4_1_8/config.ru +0 -4
- data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
- data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
- data/fixtures/rails_4_1_8/log/.keep +0 -0
- data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +0 -83
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +0 -59
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +0 -15
- data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
- data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
- data/lib/secure_headers/hash_helper.rb +0 -7
- data/lib/secure_headers/header.rb +0 -5
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +0 -22
- data/lib/secure_headers/version.rb +0 -3
- data/lib/tasks/tasks.rake +0 -48
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +0 -47
|
@@ -2,328 +2,241 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe ContentSecurityPolicy do
|
|
5
|
-
let(:default_opts) do
|
|
5
|
+
let (:default_opts) do
|
|
6
6
|
{
|
|
7
|
-
:
|
|
8
|
-
:
|
|
9
|
-
:
|
|
10
|
-
:
|
|
11
|
-
:
|
|
7
|
+
default_src: %w(https:),
|
|
8
|
+
img_src: %w(https: data:),
|
|
9
|
+
script_src: %w('unsafe-inline' 'unsafe-eval' https: data:),
|
|
10
|
+
style_src: %w('unsafe-inline' https: about:),
|
|
11
|
+
report_uri: %w(/csp_report)
|
|
12
12
|
}
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
IE = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
|
|
16
|
-
FIREFOX = "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8"
|
|
17
|
-
FIREFOX_23 = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0"
|
|
18
|
-
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"
|
|
19
|
-
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"
|
|
20
|
-
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"
|
|
21
|
-
OPERA = "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16"
|
|
22
|
-
|
|
23
|
-
def request_for user_agent, request_uri=nil, options={:ssl => false}
|
|
24
|
-
double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') )
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
before(:each) do
|
|
28
|
-
@options_with_forwarding = default_opts.merge(:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com')
|
|
29
|
-
end
|
|
30
|
-
|
|
31
15
|
describe "#name" do
|
|
32
|
-
context "when supplying options to override request" do
|
|
33
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
34
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
35
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
36
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
37
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
16
|
context "when in report-only mode" do
|
|
41
|
-
specify { expect(ContentSecurityPolicy.new(default_opts
|
|
42
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
43
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
44
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
45
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
17
|
+
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") }
|
|
46
18
|
end
|
|
47
19
|
|
|
48
20
|
context "when in enforce mode" do
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(HEADER_NAME)}
|
|
52
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME)}
|
|
53
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME)}
|
|
54
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME)}
|
|
55
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)}
|
|
21
|
+
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicy::HEADER_NAME) }
|
|
56
22
|
end
|
|
57
23
|
end
|
|
58
24
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
25
|
+
describe "#validate_config!" do
|
|
26
|
+
it "accepts all keys" do
|
|
27
|
+
# (pulled from README)
|
|
28
|
+
config = {
|
|
29
|
+
# "meta" values. these will shaped the header, but the values are not included in the header.
|
|
30
|
+
report_only: true, # default: false
|
|
31
|
+
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
32
|
+
|
|
33
|
+
# directive values: these values will directly translate into source directives
|
|
34
|
+
default_src: %w(https: 'self'),
|
|
35
|
+
frame_src: %w('self' *.twimg.com itunes.apple.com),
|
|
36
|
+
connect_src: %w(wws:),
|
|
37
|
+
font_src: %w('self' data:),
|
|
38
|
+
img_src: %w(mycdn.com data:),
|
|
39
|
+
media_src: %w(utoob.com),
|
|
40
|
+
object_src: %w('self'),
|
|
41
|
+
script_src: %w('self'),
|
|
42
|
+
style_src: %w('unsafe-inline'),
|
|
43
|
+
base_uri: %w('self'),
|
|
44
|
+
child_src: %w('self'),
|
|
45
|
+
form_action: %w('self' github.com),
|
|
46
|
+
frame_ancestors: %w('none'),
|
|
47
|
+
plugin_types: %w(application/x-shockwave-flash),
|
|
48
|
+
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
|
|
49
|
+
report_uri: %w(https://example.com/uri-directive)
|
|
50
|
+
}
|
|
75
51
|
|
|
76
|
-
|
|
77
|
-
it "adds hashes and unsafe-inline to the script-src" do
|
|
78
|
-
policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123']))
|
|
79
|
-
expect(policy.value).to match /script-src[^;]*'sha256-abc123'/
|
|
52
|
+
CSP.validate_config!(config)
|
|
80
53
|
end
|
|
81
|
-
end
|
|
82
54
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@opts = default_opts
|
|
55
|
+
it "requires a :default_src value" do
|
|
56
|
+
expect do
|
|
57
|
+
CSP.validate_config!(script_src: %('self'))
|
|
58
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
88
59
|
end
|
|
89
60
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
it "adds a @enforce and @app_name variables to the report uri" do
|
|
97
|
-
opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => proc { 'twitter' })
|
|
98
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
99
|
-
expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter")
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
it "does not add an empty @app_name variable to the report uri" do
|
|
103
|
-
opts = @opts.merge(:tag_report_uri => true, :enforce => true)
|
|
104
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
105
|
-
expect(csp.value).to include("/csp_report?enforce=true")
|
|
106
|
-
end
|
|
61
|
+
it "requires :report_only to be a truthy value" do
|
|
62
|
+
expect do
|
|
63
|
+
CSP.validate_config!(default_opts.merge(report_only: "steve"))
|
|
64
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
65
|
+
end
|
|
107
66
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
67
|
+
it "requires :preserve_schemes to be a truthy value" do
|
|
68
|
+
expect do
|
|
69
|
+
CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
|
|
70
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
71
|
+
end
|
|
113
72
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
73
|
+
it "requires :block_all_mixed_content to be a boolean value" do
|
|
74
|
+
expect do
|
|
75
|
+
CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
|
|
76
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
77
|
+
end
|
|
117
78
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
79
|
+
it "requires all source lists to be an array of strings" do
|
|
80
|
+
expect do
|
|
81
|
+
CSP.validate_config!(default_src: "steve")
|
|
82
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
83
|
+
end
|
|
124
84
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
end
|
|
85
|
+
it "allows nil values" do
|
|
86
|
+
expect do
|
|
87
|
+
CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
|
|
88
|
+
end.to_not raise_error
|
|
89
|
+
end
|
|
129
90
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
91
|
+
it "rejects unknown directives / config" do
|
|
92
|
+
expect do
|
|
93
|
+
CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
|
|
94
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
95
|
+
end
|
|
133
96
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
csp = ContentSecurityPolicy.new(opts, :controller => controller)
|
|
141
|
-
expect(csp.name).to match("Content-Security-Policy")
|
|
142
|
-
end
|
|
97
|
+
# this is mostly to ensure people don't use the antiquated shorthands common in other configs
|
|
98
|
+
it "performs light validation on source lists" do
|
|
99
|
+
expect do
|
|
100
|
+
CSP.validate_config!(default_src: %w(self none inline eval))
|
|
101
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
143
102
|
end
|
|
144
103
|
end
|
|
145
104
|
|
|
146
|
-
describe "#
|
|
147
|
-
it "
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
105
|
+
describe "#combine_policies" do
|
|
106
|
+
it "combines the default-src value with the override if the directive was unconfigured" do
|
|
107
|
+
combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
|
|
108
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
|
109
|
+
expect(csp.name).to eq(CSP::HEADER_NAME)
|
|
110
|
+
expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
|
|
152
111
|
end
|
|
153
112
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME))
|
|
161
|
-
expect(csp.value).to match(/#{directive_name} value;/)
|
|
162
|
-
end
|
|
113
|
+
it "overrides the report_only flag" do
|
|
114
|
+
Configuration.default do |config|
|
|
115
|
+
config.csp = {
|
|
116
|
+
default_src: %w('self'),
|
|
117
|
+
report_only: false
|
|
118
|
+
}
|
|
163
119
|
end
|
|
120
|
+
combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
|
|
121
|
+
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
122
|
+
expect(csp.name).to eq(CSP::REPORT_ONLY)
|
|
164
123
|
end
|
|
165
124
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
csp =
|
|
169
|
-
|
|
125
|
+
it "overrides the :block_all_mixed_content flag" do
|
|
126
|
+
Configuration.default do |config|
|
|
127
|
+
config.csp = {
|
|
128
|
+
default_src: %w(https:),
|
|
129
|
+
block_all_mixed_content: false
|
|
130
|
+
}
|
|
170
131
|
end
|
|
132
|
+
combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
|
|
133
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
|
134
|
+
expect(csp.value).to eq("default-src https:; block-all-mixed-content")
|
|
135
|
+
end
|
|
171
136
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
137
|
+
it "raises an error if appending to a OPT_OUT policy" do
|
|
138
|
+
Configuration.default do |config|
|
|
139
|
+
config.csp = OPT_OUT
|
|
175
140
|
end
|
|
141
|
+
expect do
|
|
142
|
+
CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
|
|
143
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
176
146
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
147
|
+
describe "#idempotent_additions?" do
|
|
148
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
|
|
149
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
|
|
150
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
|
|
151
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
|
|
152
|
+
|
|
153
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
|
|
154
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
|
|
155
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
|
|
156
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
|
|
157
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
|
|
158
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
|
|
159
|
+
end
|
|
181
160
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
161
|
+
describe "#value" do
|
|
162
|
+
it "discards 'none' values if any other source expressions are present" do
|
|
163
|
+
csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
|
|
164
|
+
expect(csp.value).not_to include("'none'")
|
|
186
165
|
end
|
|
187
166
|
|
|
188
|
-
it "
|
|
189
|
-
|
|
190
|
-
csp
|
|
191
|
-
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;"
|
|
192
|
-
expect(csp.value).to eq(value)
|
|
167
|
+
it "discards source expressions (besides unsafe-* and non-host source values) when * is present" do
|
|
168
|
+
csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org data: blob:))
|
|
169
|
+
expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:")
|
|
193
170
|
end
|
|
194
171
|
|
|
195
|
-
it "
|
|
196
|
-
|
|
197
|
-
|
|
172
|
+
it "minifies source expressions based on overlapping wildcards" do
|
|
173
|
+
config = {
|
|
174
|
+
default_src: %w(a.example.org b.example.org *.example.org https://*.example.org)
|
|
175
|
+
}
|
|
176
|
+
csp = ContentSecurityPolicy.new(config)
|
|
177
|
+
expect(csp.value).to eq("default-src *.example.org")
|
|
198
178
|
end
|
|
199
179
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
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;")
|
|
204
|
-
end
|
|
180
|
+
it "removes http/s schemes from hosts" do
|
|
181
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org))
|
|
182
|
+
expect(csp.value).to eq("default-src example.org")
|
|
205
183
|
end
|
|
206
184
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
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;")
|
|
211
|
-
end
|
|
185
|
+
it "does not remove schemes from report-uri values" do
|
|
186
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org))
|
|
187
|
+
expect(csp.value).to eq("default-src https:; report-uri https://example.org")
|
|
212
188
|
end
|
|
213
189
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
it "adds a nonce and unsafe-inline to the script-src value when using firefox" do
|
|
221
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(FIREFOX))
|
|
222
|
-
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
it "adds a nonce and unsafe-inline to the script-src value when using opera" do
|
|
226
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(OPERA))
|
|
227
|
-
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
it "does not add a nonce and unsafe-inline to the script-src value when using Safari" do
|
|
231
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(SAFARI))
|
|
232
|
-
expect(header.value).to include("script-src 'self' 'unsafe-inline'")
|
|
233
|
-
expect(header.value).not_to include("nonce")
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
it "does not add a nonce and unsafe-inline to the script-src value when using IE" do
|
|
237
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(IE))
|
|
238
|
-
expect(header.value).to include("script-src 'self' 'unsafe-inline'")
|
|
239
|
-
expect(header.value).not_to include("nonce")
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
it "adds a nonce and unsafe-inline to the style-src value" do
|
|
243
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME))
|
|
244
|
-
expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
245
|
-
end
|
|
190
|
+
it "does not remove schemes when :preserve_schemes is true" do
|
|
191
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :preserve_schemes => true)
|
|
192
|
+
expect(csp.value).to eq("default-src https://example.org")
|
|
193
|
+
end
|
|
246
194
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
|
252
|
-
expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
|
253
|
-
end
|
|
195
|
+
it "removes nil from source lists" do
|
|
196
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org", nil])
|
|
197
|
+
expect(csp.value).to eq("default-src example.org")
|
|
198
|
+
end
|
|
254
199
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
end
|
|
200
|
+
it "does not add a directive if the value is an empty array (or all nil)" do
|
|
201
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: [nil])
|
|
202
|
+
expect(csp.value).to eq("default-src example.org")
|
|
259
203
|
end
|
|
260
204
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
:frame_src => "http:",
|
|
266
|
-
:img_src => "http:"
|
|
267
|
-
}
|
|
268
|
-
})
|
|
269
|
-
}
|
|
205
|
+
it "does not add a directive if the value is nil" do
|
|
206
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: nil)
|
|
207
|
+
expect(csp.value).to eq("default-src example.org")
|
|
208
|
+
end
|
|
270
209
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
210
|
+
it "deduplicates any source expressions" do
|
|
211
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org))
|
|
212
|
+
expect(csp.value).to eq("default-src example.org")
|
|
213
|
+
end
|
|
275
214
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
215
|
+
context "browser sniffing" do
|
|
216
|
+
let (:complex_opts) do
|
|
217
|
+
ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = %w('self') }
|
|
218
|
+
.merge(block_all_mixed_content: true, reflected_xss: "block")
|
|
219
|
+
.merge(script_src: %w('self'), script_nonce: 123456)
|
|
279
220
|
end
|
|
280
221
|
|
|
281
|
-
it "does not
|
|
282
|
-
|
|
283
|
-
expect(
|
|
222
|
+
it "does not filter any directives for Chrome" do
|
|
223
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
224
|
+
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'")
|
|
284
225
|
end
|
|
285
|
-
end
|
|
286
226
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
double.tap do |env|
|
|
291
|
-
allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
let(:request) do
|
|
295
|
-
double(
|
|
296
|
-
:ssl? => true,
|
|
297
|
-
:url => 'https://example.com',
|
|
298
|
-
:env => env
|
|
299
|
-
)
|
|
227
|
+
it "does not filter any directives for Opera" do
|
|
228
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
229
|
+
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'")
|
|
300
230
|
end
|
|
301
231
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
let(:options) { {:controller => controller} }
|
|
306
|
-
|
|
307
|
-
it "adds metadata to env" do
|
|
308
|
-
metadata = {
|
|
309
|
-
:config => config,
|
|
310
|
-
:options => options
|
|
311
|
-
}
|
|
312
|
-
expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
|
|
313
|
-
expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
|
|
314
|
-
ContentSecurityPolicy.add_to_env(request, controller, config)
|
|
315
|
-
end
|
|
232
|
+
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
233
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
234
|
+
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'")
|
|
316
235
|
end
|
|
317
236
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
expect(options).to eql({
|
|
322
|
-
:ua => ua,
|
|
323
|
-
:ssl => true,
|
|
324
|
-
:request_uri => 'https://example.com'
|
|
325
|
-
})
|
|
326
|
-
end
|
|
237
|
+
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
|
|
238
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
239
|
+
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'")
|
|
327
240
|
end
|
|
328
241
|
end
|
|
329
242
|
end
|
|
@@ -2,35 +2,35 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe PublicKeyPins do
|
|
5
|
-
specify{ expect(PublicKeyPins.new(:
|
|
6
|
-
specify{ expect(PublicKeyPins.new(:
|
|
5
|
+
specify { expect(PublicKeyPins.new(max_age: 1234, report_only: true).name).to eq("Public-Key-Pins-Report-Only") }
|
|
6
|
+
specify { expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins") }
|
|
7
7
|
|
|
8
|
-
specify { expect(PublicKeyPins.new(
|
|
9
|
-
specify { expect(PublicKeyPins.new(:
|
|
10
|
-
specify
|
|
11
|
-
config = {:
|
|
8
|
+
specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
|
|
9
|
+
specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
|
|
10
|
+
specify do
|
|
11
|
+
config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin1' }, { sha256: 'base64encodedpin2' }] }
|
|
12
12
|
header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\""
|
|
13
13
|
expect(PublicKeyPins.new(config).value).to eq(header_value)
|
|
14
|
-
|
|
14
|
+
end
|
|
15
15
|
|
|
16
16
|
context "with an invalid configuration" do
|
|
17
17
|
it "raises an exception when max-age is not provided" do
|
|
18
|
-
expect
|
|
19
|
-
PublicKeyPins.
|
|
20
|
-
|
|
18
|
+
expect do
|
|
19
|
+
PublicKeyPins.validate_config!(foo: 'bar')
|
|
20
|
+
end.to raise_error(PublicKeyPinsConfigError)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
it "raises an exception with an invalid max-age" do
|
|
24
|
-
expect
|
|
25
|
-
PublicKeyPins.
|
|
26
|
-
|
|
24
|
+
expect do
|
|
25
|
+
PublicKeyPins.validate_config!(max_age: 'abc123')
|
|
26
|
+
end.to raise_error(PublicKeyPinsConfigError)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
it 'raises an exception with less than 2 pins' do
|
|
30
|
-
expect
|
|
31
|
-
config = {:
|
|
32
|
-
PublicKeyPins.
|
|
33
|
-
|
|
30
|
+
expect do
|
|
31
|
+
config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin' }] }
|
|
32
|
+
PublicKeyPins.validate_config!(config)
|
|
33
|
+
end.to raise_error(PublicKeyPinsConfigError)
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
end
|