secure_headers 1.1.1 → 2.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.
- data/.gitignore +4 -8
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -3
- data/Gemfile +6 -11
- data/README.md +184 -101
- data/Rakefile +11 -116
- 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 +1 -4
- data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
- 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 +3 -1
- data/fixtures/rails_3_2_12/config/routes.rb +0 -57
- data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
- data/fixtures/rails_3_2_12/config.ru +3 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +57 -19
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +18 -13
- 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/other_things_controller.rb +1 -2
- 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 +17 -12
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +17 -12
- data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -18
- data/fixtures/rails_4_1_8/Gemfile +5 -0
- data/fixtures/rails_4_1_8/README.rdoc +28 -0
- data/fixtures/rails_4_1_8/Rakefile +6 -0
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
- 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 +11 -0
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
- data/fixtures/rails_4_1_8/config/application.rb +15 -0
- data/fixtures/rails_4_1_8/config/boot.rb +4 -0
- data/fixtures/rails_4_1_8/config/environment.rb +5 -0
- data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
- data/fixtures/rails_4_1_8/config/routes.rb +4 -0
- data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
- data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
- data/fixtures/rails_4_1_8/config.ru +4 -0
- 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 +83 -0
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
- 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 +7 -0
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
- data/lib/secure_headers/headers/content_security_policy.rb +154 -129
- 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_permitted_cross_domain_policies.rb +40 -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 +19 -24
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +68 -0
- data/lib/secure_headers.rb +65 -16
- data/lib/tasks/tasks.rake +48 -0
- data/secure_headers.gemspec +4 -4
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +127 -220
- 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_permitted_cross_domain_policies_spec.rb +64 -0
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +20 -19
- data/spec/lib/secure_headers_spec.rb +69 -68
- data/spec/spec_helper.rb +29 -18
- metadata +51 -46
- data/.rvmrc +0 -1
- data/Guardfile +0 -10
- data/HISTORY.md +0 -115
- data/app/controllers/content_security_policy_controller.rb +0 -75
- data/config/curl-ca-bundle.crt +0 -5420
- data/config/routes.rb +0 -3
- 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
- data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
|
@@ -4,12 +4,11 @@ module SecureHeaders
|
|
|
4
4
|
describe ContentSecurityPolicy do
|
|
5
5
|
let(:default_opts) do
|
|
6
6
|
{
|
|
7
|
-
:disable_chrome_extension => true,
|
|
8
7
|
:disable_fill_missing => true,
|
|
9
|
-
:default_src => 'https
|
|
8
|
+
:default_src => 'https:',
|
|
10
9
|
:report_uri => '/csp_report',
|
|
11
|
-
:script_src => 'inline eval https
|
|
12
|
-
:style_src => "inline https
|
|
10
|
+
:script_src => 'inline eval https: data:',
|
|
11
|
+
:style_src => "inline https: about:"
|
|
13
12
|
}
|
|
14
13
|
end
|
|
15
14
|
|
|
@@ -30,44 +29,41 @@ module SecureHeaders
|
|
|
30
29
|
|
|
31
30
|
describe "#name" do
|
|
32
31
|
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.
|
|
32
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
33
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
34
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
35
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
36
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
38
37
|
end
|
|
39
38
|
|
|
40
39
|
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.
|
|
40
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
41
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
42
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
43
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
44
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
context "when in enforce mode" do
|
|
49
48
|
let(:opts) { default_opts.merge(:enforce => true)}
|
|
50
49
|
|
|
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.
|
|
50
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(HEADER_NAME)}
|
|
51
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME)}
|
|
52
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME)}
|
|
53
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME)}
|
|
54
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)}
|
|
56
55
|
end
|
|
56
|
+
end
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
specify { ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX_23)}).name.should == STANDARD_HEADER_NAME + "-Report-Only"}
|
|
63
|
-
specify { ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME)}).name.should == STANDARD_HEADER_NAME + "-Report-Only"}
|
|
64
|
-
specify { ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME_25)}).name.should == STANDARD_HEADER_NAME + "-Report-Only"}
|
|
58
|
+
context "when using hash sources" do
|
|
59
|
+
it "adds hashes and unsafe-inline to the script-src" do
|
|
60
|
+
policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123']))
|
|
61
|
+
expect(policy.value).to match /script-src[^;]*'sha256-abc123'/
|
|
65
62
|
end
|
|
66
63
|
end
|
|
67
64
|
|
|
68
65
|
describe "#normalize_csp_options" do
|
|
69
66
|
before(:each) do
|
|
70
|
-
default_opts.delete(:disable_chrome_extension)
|
|
71
67
|
default_opts.delete(:disable_fill_missing)
|
|
72
68
|
default_opts[:script_src] << ' self none'
|
|
73
69
|
@opts = default_opts
|
|
@@ -76,115 +72,41 @@ module SecureHeaders
|
|
|
76
72
|
context "Content-Security-Policy" do
|
|
77
73
|
it "converts the script values to their equivilents" do
|
|
78
74
|
csp = ContentSecurityPolicy.new(@opts, :request => request_for(CHROME))
|
|
79
|
-
csp.value.
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
describe "#same_origin?" do
|
|
85
|
-
let(:origin) {"https://example.com:123"}
|
|
86
|
-
|
|
87
|
-
it "matches when host, scheme, and port match" do
|
|
88
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com"))
|
|
89
|
-
csp.send(:same_origin?).should be_true
|
|
90
|
-
|
|
91
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com:443"))
|
|
92
|
-
csp.send(:same_origin?).should be_true
|
|
93
|
-
|
|
94
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com:123'}, :request => request_for(FIREFOX, "https://example.com:123"))
|
|
95
|
-
csp.send(:same_origin?).should be_true
|
|
96
|
-
|
|
97
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
98
|
-
csp.send(:same_origin?).should be_true
|
|
99
|
-
|
|
100
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com:80'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
101
|
-
csp.send(:same_origin?).should be_true
|
|
102
|
-
|
|
103
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:80"))
|
|
104
|
-
csp.send(:same_origin?).should be_true
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
it "does not match port mismatches" do
|
|
108
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:81"))
|
|
109
|
-
csp.send(:same_origin?).should be_false
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
it "does not match host mismatches" do
|
|
113
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://twitter.com'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
114
|
-
csp.send(:same_origin?).should be_false
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
it "does not match host mismatches because of subdomains" do
|
|
118
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://sub.example.com"))
|
|
119
|
-
csp.send(:same_origin?).should be_false
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
it "does not match scheme mismatches" do
|
|
123
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "ftp://example.com"))
|
|
124
|
-
csp.send(:same_origin?).should be_false
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
it "does not match on substring collisions" do
|
|
128
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://anotherexample.com"))
|
|
129
|
-
csp.send(:same_origin?).should be_false
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
describe "#normalize_reporting_endpoint" do
|
|
134
|
-
let(:opts) {{:report_uri => 'https://example.com/csp', :forward_endpoint => anything}}
|
|
135
|
-
|
|
136
|
-
context "when using firefox" do
|
|
137
|
-
it "updates the report-uri when posting to a different host" do
|
|
138
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://anexample.com"))
|
|
139
|
-
csp.report_uri.should == FF_CSP_ENDPOINT
|
|
75
|
+
expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https: data: 'self' 'none'")
|
|
140
76
|
end
|
|
141
77
|
|
|
142
|
-
it "
|
|
143
|
-
|
|
144
|
-
csp.
|
|
78
|
+
it "adds a @enforce and @app_name variables to the report uri" do
|
|
79
|
+
opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => lambda { 'twitter' })
|
|
80
|
+
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
81
|
+
expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter")
|
|
145
82
|
end
|
|
146
83
|
|
|
147
|
-
it "
|
|
148
|
-
|
|
149
|
-
csp.
|
|
84
|
+
it "does not add an empty @app_name variable to the report uri" do
|
|
85
|
+
opts = @opts.merge(:tag_report_uri => true, :enforce => true)
|
|
86
|
+
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
87
|
+
expect(csp.value).to include("/csp_report?enforce=true")
|
|
150
88
|
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
it "does not update the URI is the report_uri is on the same origin" do
|
|
154
|
-
opts = {:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com'}
|
|
155
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://example.com/somewhere"))
|
|
156
|
-
csp.report_uri.should == 'https://example.com/csp'
|
|
157
|
-
end
|
|
158
89
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
csp.report_uri.should == 'https://example.com/csp'
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
context "when using a protocol-relative value for report-uri" do
|
|
165
|
-
let(:opts) {
|
|
166
|
-
{
|
|
90
|
+
it "accepts procs for report-uris" do
|
|
91
|
+
opts = {
|
|
167
92
|
:default_src => 'self',
|
|
168
|
-
:report_uri =>
|
|
93
|
+
:report_uri => lambda { "http://lambda/result" }
|
|
169
94
|
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
it "uses the current protocol" do
|
|
173
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, '/', :ssl => true))
|
|
174
|
-
csp.value.should =~ %r{report-uri https://example.com/csp;}
|
|
175
95
|
|
|
176
|
-
csp = ContentSecurityPolicy.new(opts
|
|
177
|
-
csp.value.
|
|
96
|
+
csp = ContentSecurityPolicy.new(opts)
|
|
97
|
+
expect(csp.value).to match("report-uri http://lambda/result")
|
|
178
98
|
end
|
|
179
99
|
|
|
180
|
-
it "
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
100
|
+
it "accepts procs for other fields" do
|
|
101
|
+
opts = {
|
|
102
|
+
:default_src => lambda { "http://lambda/result" },
|
|
103
|
+
:enforce => lambda { true },
|
|
104
|
+
:disable_fill_missing => lambda { true }
|
|
105
|
+
}
|
|
184
106
|
|
|
185
|
-
|
|
186
|
-
csp
|
|
187
|
-
csp.
|
|
107
|
+
csp = ContentSecurityPolicy.new(opts)
|
|
108
|
+
expect(csp.value).to eq("default-src http://lambda/result; img-src http://lambda/result data:;")
|
|
109
|
+
expect(csp.name).to match("Content-Security-Policy")
|
|
188
110
|
end
|
|
189
111
|
end
|
|
190
112
|
end
|
|
@@ -192,73 +114,83 @@ module SecureHeaders
|
|
|
192
114
|
describe "#value" do
|
|
193
115
|
it "raises an exception when default-src is missing" do
|
|
194
116
|
csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME))
|
|
195
|
-
|
|
117
|
+
expect {
|
|
196
118
|
csp.value
|
|
197
|
-
}.
|
|
119
|
+
}.to raise_error(RuntimeError)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
context "CSP level 2 directives" do
|
|
123
|
+
let(:config) { {:default_src => 'self'} }
|
|
124
|
+
::SecureHeaders::ContentSecurityPolicy::Constants::NON_DEFAULT_SOURCES.each do |non_default_source|
|
|
125
|
+
it "supports all level 2 directives" do
|
|
126
|
+
directive_name = ::SecureHeaders::ContentSecurityPolicy.send(:symbol_to_hyphen_case, non_default_source)
|
|
127
|
+
config.merge!({ non_default_source => "value" })
|
|
128
|
+
csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME))
|
|
129
|
+
expect(csp.value).to match(/#{directive_name} value;/)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
198
132
|
end
|
|
199
133
|
|
|
200
134
|
context "auto-whitelists data: uris for img-src" do
|
|
201
135
|
it "sets the value if no img-src specified" do
|
|
202
|
-
csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true
|
|
203
|
-
csp.value.
|
|
136
|
+
csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
|
|
137
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
204
138
|
end
|
|
205
139
|
|
|
206
140
|
it "appends the value if img-src is specified" do
|
|
207
|
-
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true
|
|
208
|
-
csp.value.
|
|
141
|
+
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
|
|
142
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
209
143
|
end
|
|
210
144
|
end
|
|
211
145
|
|
|
212
146
|
it "fills in directives without values with default-src value" do
|
|
213
147
|
options = default_opts.merge(:disable_fill_missing => false)
|
|
214
148
|
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
215
|
-
value = "default-src https
|
|
216
|
-
csp.value.
|
|
149
|
+
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;"
|
|
150
|
+
expect(csp.value).to eq(value)
|
|
217
151
|
end
|
|
218
152
|
|
|
219
153
|
it "sends the standard csp header if an unknown browser is supplied" do
|
|
220
154
|
csp = ContentSecurityPolicy.new(default_opts, :request => request_for(IE))
|
|
221
|
-
csp.value.
|
|
155
|
+
expect(csp.value).to match "default-src"
|
|
222
156
|
end
|
|
223
157
|
|
|
224
158
|
context "Firefox" do
|
|
225
159
|
it "builds a csp header for firefox" do
|
|
226
160
|
csp = ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX))
|
|
227
|
-
csp.value.
|
|
161
|
+
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
162
|
end
|
|
229
163
|
end
|
|
230
164
|
|
|
231
165
|
context "Chrome" do
|
|
232
166
|
it "builds a csp header for chrome" do
|
|
233
167
|
csp = ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME))
|
|
234
|
-
csp.value.
|
|
168
|
+
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
169
|
end
|
|
170
|
+
end
|
|
236
171
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
172
|
+
context "when using a nonce" do
|
|
173
|
+
it "adds a nonce and unsafe-inline to the script-src value" do
|
|
174
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME))
|
|
175
|
+
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
240
176
|
end
|
|
241
|
-
end
|
|
242
177
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
:default_src => 'self',
|
|
248
|
-
:script_src => 'https://*',
|
|
249
|
-
:experimental => {
|
|
250
|
-
:script_src => 'self'
|
|
251
|
-
}
|
|
252
|
-
}}
|
|
178
|
+
it "adds a nonce and unsafe-inline to the style-src value" do
|
|
179
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME))
|
|
180
|
+
expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
181
|
+
end
|
|
253
182
|
|
|
254
|
-
it "
|
|
255
|
-
header = ContentSecurityPolicy.new(
|
|
256
|
-
header.
|
|
183
|
+
it "adds an identical nonce to the style and script-src directives" do
|
|
184
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME))
|
|
185
|
+
nonce = header.nonce
|
|
186
|
+
value = header.value
|
|
187
|
+
expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
|
188
|
+
expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
|
257
189
|
end
|
|
258
190
|
|
|
259
|
-
it "
|
|
260
|
-
header = ContentSecurityPolicy.new(
|
|
261
|
-
header.value.
|
|
191
|
+
it "does not add 'unsafe-inline' twice" do
|
|
192
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME))
|
|
193
|
+
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';")
|
|
262
194
|
end
|
|
263
195
|
end
|
|
264
196
|
|
|
@@ -266,93 +198,68 @@ module SecureHeaders
|
|
|
266
198
|
let(:options) {
|
|
267
199
|
default_opts.merge({
|
|
268
200
|
:http_additions => {
|
|
269
|
-
:frame_src => "http
|
|
270
|
-
:img_src => "http
|
|
201
|
+
:frame_src => "http:",
|
|
202
|
+
:img_src => "http:"
|
|
271
203
|
}
|
|
272
204
|
})
|
|
273
205
|
}
|
|
274
206
|
|
|
275
207
|
it "adds directive values for headers on http" do
|
|
276
208
|
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
277
|
-
csp.value.
|
|
209
|
+
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
210
|
end
|
|
279
211
|
|
|
280
212
|
it "does not add the directive values if requesting https" do
|
|
281
213
|
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME, '/', :ssl => true))
|
|
282
|
-
csp.value.
|
|
214
|
+
expect(csp.value).not_to match(/http:/)
|
|
283
215
|
end
|
|
284
216
|
|
|
285
217
|
it "does not add the directive values if requesting https" do
|
|
286
218
|
csp = ContentSecurityPolicy.new(options, :ua => "Chrome", :ssl => true)
|
|
287
|
-
csp.value.
|
|
219
|
+
expect(csp.value).not_to match(/http:/)
|
|
288
220
|
end
|
|
221
|
+
end
|
|
289
222
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let(:options) {{
|
|
296
|
-
:disable_chrome_extension => true,
|
|
297
|
-
:disable_fill_missing => true,
|
|
298
|
-
:default_src => 'self',
|
|
299
|
-
:script_src => 'https://*',
|
|
300
|
-
:http_additions => {
|
|
301
|
-
:script_src => 'http://*'
|
|
302
|
-
},
|
|
303
|
-
:experimental => {
|
|
304
|
-
:script_src => 'self',
|
|
305
|
-
:http_additions => {
|
|
306
|
-
:script_src => 'https://mycdn.example.com'
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}}
|
|
310
|
-
# for comparison purposes, if not using the experimental header this would produce
|
|
311
|
-
# "allow 'self'; script-src https://*" for https requests
|
|
312
|
-
# and
|
|
313
|
-
# "allow 'self'; script-src https://* http://*" for http requests
|
|
314
|
-
|
|
315
|
-
it "uses the value in the experimental block over SSL" do
|
|
316
|
-
csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX, '/', :ssl => true))
|
|
317
|
-
csp.value.should == "default-src 'self'; img-src data:; script-src 'self';"
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
it "detects the :ssl => true option" do
|
|
321
|
-
csp = ContentSecurityPolicy.new(options, :experimental => true, :ua => FIREFOX, :ssl => true)
|
|
322
|
-
csp.value.should == "default-src 'self'; img-src data:; script-src 'self';"
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
it "merges the values from experimental/http_additions when not over SSL" do
|
|
326
|
-
csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX))
|
|
327
|
-
csp.value.should == "default-src 'self'; img-src data:; script-src 'self' https://mycdn.example.com;"
|
|
223
|
+
describe "class methods" do
|
|
224
|
+
let(:ua) { CHROME }
|
|
225
|
+
let(:env) do
|
|
226
|
+
double.tap do |env|
|
|
227
|
+
allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
|
|
328
228
|
end
|
|
329
229
|
end
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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;"
|
|
230
|
+
let(:request) do
|
|
231
|
+
double(
|
|
232
|
+
:ssl? => true,
|
|
233
|
+
:url => 'https://example.com',
|
|
234
|
+
:env => env
|
|
235
|
+
)
|
|
342
236
|
end
|
|
343
237
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
238
|
+
describe ".add_to_env" do
|
|
239
|
+
let(:controller) { double }
|
|
240
|
+
let(:config) { {:default_src => 'self'} }
|
|
241
|
+
let(:options) { {:controller => controller} }
|
|
242
|
+
|
|
243
|
+
it "adds metadata to env" do
|
|
244
|
+
metadata = {
|
|
245
|
+
:config => config,
|
|
246
|
+
:options => options
|
|
247
|
+
}
|
|
248
|
+
expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
|
|
249
|
+
expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
|
|
250
|
+
ContentSecurityPolicy.add_to_env(request, controller, config)
|
|
251
|
+
end
|
|
348
252
|
end
|
|
349
253
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
254
|
+
describe ".options_from_request" do
|
|
255
|
+
it "extracts options from request" do
|
|
256
|
+
options = ContentSecurityPolicy.options_from_request(request)
|
|
257
|
+
expect(options).to eql({
|
|
258
|
+
:ua => ua,
|
|
259
|
+
:ssl => true,
|
|
260
|
+
:request_uri => 'https://example.com'
|
|
261
|
+
})
|
|
262
|
+
end
|
|
356
263
|
end
|
|
357
264
|
end
|
|
358
265
|
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
|
+
XDownloadOptions.new("open")
|
|
28
|
+
}.to raise_error
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|