secure_headers 3.0.3 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  module SecureHeaders
2
2
  class XContentTypeOptionsConfigError < StandardError; end
3
- # IE only
3
+
4
4
  class XContentTypeOptions
5
5
  HEADER_NAME = "X-Content-Type-Options"
6
6
  DEFAULT_VALUE = "nosniff"
@@ -1,5 +1,7 @@
1
1
  module SecureHeaders
2
2
  class Middleware
3
+ SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze
4
+
3
5
  def initialize(app)
4
6
  @app = app
5
7
  end
@@ -8,8 +10,29 @@ module SecureHeaders
8
10
  def call(env)
9
11
  req = Rack::Request.new(env)
10
12
  status, headers, response = @app.call(env)
13
+
14
+ config = SecureHeaders.config_for(req)
15
+ flag_cookies_as_secure!(headers) if config.secure_cookies
11
16
  headers.merge!(SecureHeaders.header_hash_for(req))
12
17
  [status, headers, response]
13
18
  end
19
+
20
+ private
21
+
22
+ # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
23
+ def flag_cookies_as_secure!(headers)
24
+ if cookies = headers['Set-Cookie']
25
+ # Support Rails 2.3 / Rack 1.1 arrays as headers
26
+ cookies = cookies.split("\n") unless cookies.is_a?(Array)
27
+
28
+ headers['Set-Cookie'] = cookies.map do |cookie|
29
+ if cookie !~ SECURE_COOKIE_REGEXP
30
+ "#{cookie}; secure"
31
+ else
32
+ cookie
33
+ end
34
+ end.join("\n")
35
+ end
36
+ end
14
37
  end
15
38
  end
@@ -10,7 +10,7 @@ if defined?(Rails::Railtie)
10
10
  'Public-Key-Pins', 'Public-Key-Pins-Report-Only']
11
11
 
12
12
  initializer "secure_headers.middleware" do
13
- Rails.application.config.middleware.use SecureHeaders::Middleware
13
+ Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
14
14
  end
15
15
 
16
16
  initializer "secure_headers.action_controller" do
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |gem|
3
3
  gem.name = "secure_headers"
4
- gem.version = "3.0.3"
4
+ gem.version = "3.1.0"
5
5
  gem.authors = ["Neil Matatall"]
6
6
  gem.email = ["neil.matatall@gmail.com"]
7
7
  gem.description = 'Security related headers all in one gem.'
@@ -29,16 +29,14 @@ module SecureHeaders
29
29
  expect_default_values(header_hash)
30
30
  end
31
31
 
32
- it "copies all config values except for the cached headers when dup" do
32
+ it "copies config values when duping" do
33
33
  Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do
34
34
  # do nothing, just copy it
35
35
  end
36
36
 
37
37
  config = Configuration.get(:test_override)
38
38
  noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
39
- [:hsts, :x_frame_options, :x_content_type_options, :x_xss_protection,
40
- :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp].each do |key|
41
-
39
+ [:csp, :dynamic_csp, :secure_cookies].each do |key|
42
40
  expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
43
41
  end
44
42
  end
@@ -22,181 +22,6 @@ module SecureHeaders
22
22
  end
23
23
  end
24
24
 
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
- upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
50
- report_uri: %w(https://example.com/uri-directive)
51
- }
52
-
53
- CSP.validate_config!(config)
54
- end
55
-
56
- it "requires a :default_src value" do
57
- expect do
58
- CSP.validate_config!(script_src: %('self'))
59
- end.to raise_error(ContentSecurityPolicyConfigError)
60
- end
61
-
62
- it "requires :report_only to be a truthy value" do
63
- expect do
64
- CSP.validate_config!(default_opts.merge(report_only: "steve"))
65
- end.to raise_error(ContentSecurityPolicyConfigError)
66
- end
67
-
68
- it "requires :preserve_schemes to be a truthy value" do
69
- expect do
70
- CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
71
- end.to raise_error(ContentSecurityPolicyConfigError)
72
- end
73
-
74
- it "requires :block_all_mixed_content to be a boolean value" do
75
- expect do
76
- CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
77
- end.to raise_error(ContentSecurityPolicyConfigError)
78
- end
79
-
80
- it "requires :upgrade_insecure_requests to be a boolean value" do
81
- expect do
82
- CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve"))
83
- end.to raise_error(ContentSecurityPolicyConfigError)
84
- end
85
-
86
- it "requires all source lists to be an array of strings" do
87
- expect do
88
- CSP.validate_config!(default_src: "steve")
89
- end.to raise_error(ContentSecurityPolicyConfigError)
90
- end
91
-
92
- it "allows nil values" do
93
- expect do
94
- CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
95
- end.to_not raise_error
96
- end
97
-
98
- it "rejects unknown directives / config" do
99
- expect do
100
- CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
101
- end.to raise_error(ContentSecurityPolicyConfigError)
102
- end
103
-
104
- # this is mostly to ensure people don't use the antiquated shorthands common in other configs
105
- it "performs light validation on source lists" do
106
- expect do
107
- CSP.validate_config!(default_src: %w(self none inline eval))
108
- end.to raise_error(ContentSecurityPolicyConfigError)
109
- end
110
- end
111
-
112
- describe "#combine_policies" do
113
- it "combines the default-src value with the override if the directive was unconfigured" do
114
- combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
115
- csp = ContentSecurityPolicy.new(combined_config)
116
- expect(csp.name).to eq(CSP::HEADER_NAME)
117
- expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
118
- end
119
-
120
- it "combines directives where the original value is nil and the hash is frozen" do
121
- Configuration.default do |config|
122
- config.csp = {
123
- default_src: %w('self'),
124
- report_only: false
125
- }.freeze
126
- end
127
- report_uri = "https://report-uri.io/asdf"
128
- combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri])
129
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
130
- expect(csp.value).to include("report-uri #{report_uri}")
131
- end
132
-
133
- it "does not combine the default-src value for directives that don't fall back to default sources" do
134
- Configuration.default do |config|
135
- config.csp = {
136
- default_src: %w('self'),
137
- report_only: false
138
- }.freeze
139
- end
140
- non_default_source_additions = CSP::NON_DEFAULT_SOURCES.each_with_object({}) do |directive, hash|
141
- hash[directive] = %w("http://example.org)
142
- end
143
- combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions)
144
-
145
- CSP::NON_DEFAULT_SOURCES.each do |directive|
146
- expect(combined_config[directive]).to eq(%w("http://example.org))
147
- end
148
-
149
- ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value
150
- end
151
-
152
- it "overrides the report_only flag" do
153
- Configuration.default do |config|
154
- config.csp = {
155
- default_src: %w('self'),
156
- report_only: false
157
- }
158
- end
159
- combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
160
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
161
- expect(csp.name).to eq(CSP::REPORT_ONLY)
162
- end
163
-
164
- it "overrides the :block_all_mixed_content flag" do
165
- Configuration.default do |config|
166
- config.csp = {
167
- default_src: %w(https:),
168
- block_all_mixed_content: false
169
- }
170
- end
171
- combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
172
- csp = ContentSecurityPolicy.new(combined_config)
173
- expect(csp.value).to eq("default-src https:; block-all-mixed-content")
174
- end
175
-
176
- it "raises an error if appending to a OPT_OUT policy" do
177
- Configuration.default do |config|
178
- config.csp = OPT_OUT
179
- end
180
- expect do
181
- CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
182
- end.to raise_error(ContentSecurityPolicyConfigError)
183
- end
184
- end
185
-
186
- describe "#idempotent_additions?" do
187
- specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
188
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
189
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
190
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
191
-
192
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
193
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
194
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
195
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
196
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
197
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
198
- end
199
-
200
25
  describe "#value" do
201
26
  it "discards 'none' values if any other source expressions are present" do
202
27
  csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
@@ -0,0 +1,190 @@
1
+ require 'spec_helper'
2
+
3
+ module SecureHeaders
4
+ describe PolicyManagement do
5
+ let (:default_opts) do
6
+ {
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
+ }
13
+ end
14
+
15
+ describe "#validate_config!" do
16
+ it "accepts all keys" do
17
+ # (pulled from README)
18
+ config = {
19
+ # "meta" values. these will shaped the header, but the values are not included in the header.
20
+ report_only: true, # default: false
21
+ preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
22
+
23
+ # directive values: these values will directly translate into source directives
24
+ default_src: %w(https: 'self'),
25
+ frame_src: %w('self' *.twimg.com itunes.apple.com),
26
+ connect_src: %w(wws:),
27
+ font_src: %w('self' data:),
28
+ img_src: %w(mycdn.com data:),
29
+ media_src: %w(utoob.com),
30
+ object_src: %w('self'),
31
+ script_src: %w('self'),
32
+ style_src: %w('unsafe-inline'),
33
+ base_uri: %w('self'),
34
+ child_src: %w('self'),
35
+ form_action: %w('self' github.com),
36
+ frame_ancestors: %w('none'),
37
+ plugin_types: %w(application/x-shockwave-flash),
38
+ block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
39
+ upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
40
+ report_uri: %w(https://example.com/uri-directive)
41
+ }
42
+
43
+ CSP.validate_config!(config)
44
+ end
45
+
46
+ it "requires a :default_src value" do
47
+ expect do
48
+ CSP.validate_config!(script_src: %('self'))
49
+ end.to raise_error(ContentSecurityPolicyConfigError)
50
+ end
51
+
52
+ it "requires :report_only to be a truthy value" do
53
+ expect do
54
+ CSP.validate_config!(default_opts.merge(report_only: "steve"))
55
+ end.to raise_error(ContentSecurityPolicyConfigError)
56
+ end
57
+
58
+ it "requires :preserve_schemes to be a truthy value" do
59
+ expect do
60
+ CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
61
+ end.to raise_error(ContentSecurityPolicyConfigError)
62
+ end
63
+
64
+ it "requires :block_all_mixed_content to be a boolean value" do
65
+ expect do
66
+ CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
67
+ end.to raise_error(ContentSecurityPolicyConfigError)
68
+ end
69
+
70
+ it "requires :upgrade_insecure_requests to be a boolean value" do
71
+ expect do
72
+ CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve"))
73
+ end.to raise_error(ContentSecurityPolicyConfigError)
74
+ end
75
+
76
+ it "requires all source lists to be an array of strings" do
77
+ expect do
78
+ CSP.validate_config!(default_src: "steve")
79
+ end.to raise_error(ContentSecurityPolicyConfigError)
80
+ end
81
+
82
+ it "allows nil values" do
83
+ expect do
84
+ CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
85
+ end.to_not raise_error
86
+ end
87
+
88
+ it "rejects unknown directives / config" do
89
+ expect do
90
+ CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
91
+ end.to raise_error(ContentSecurityPolicyConfigError)
92
+ end
93
+
94
+ # this is mostly to ensure people don't use the antiquated shorthands common in other configs
95
+ it "performs light validation on source lists" do
96
+ expect do
97
+ CSP.validate_config!(default_src: %w(self none inline eval))
98
+ end.to raise_error(ContentSecurityPolicyConfigError)
99
+ end
100
+ end
101
+
102
+ describe "#combine_policies" do
103
+ it "combines the default-src value with the override if the directive was unconfigured" do
104
+ combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
105
+ csp = ContentSecurityPolicy.new(combined_config)
106
+ expect(csp.name).to eq(CSP::HEADER_NAME)
107
+ expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
108
+ end
109
+
110
+ it "combines directives where the original value is nil and the hash is frozen" do
111
+ Configuration.default do |config|
112
+ config.csp = {
113
+ default_src: %w('self'),
114
+ report_only: false
115
+ }.freeze
116
+ end
117
+ report_uri = "https://report-uri.io/asdf"
118
+ combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri])
119
+ csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
120
+ expect(csp.value).to include("report-uri #{report_uri}")
121
+ end
122
+
123
+ it "does not combine the default-src value for directives that don't fall back to default sources" do
124
+ Configuration.default do |config|
125
+ config.csp = {
126
+ default_src: %w('self'),
127
+ report_only: false
128
+ }.freeze
129
+ end
130
+ non_default_source_additions = CSP::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
131
+ hash[directive] = %w("http://example.org)
132
+ end
133
+ combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions)
134
+
135
+ CSP::NON_FETCH_SOURCES.each do |directive|
136
+ expect(combined_config[directive]).to eq(%w("http://example.org))
137
+ end
138
+
139
+ ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value
140
+ end
141
+
142
+ it "overrides the report_only flag" do
143
+ Configuration.default do |config|
144
+ config.csp = {
145
+ default_src: %w('self'),
146
+ report_only: false
147
+ }
148
+ end
149
+ combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
150
+ csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
151
+ expect(csp.name).to eq(CSP::REPORT_ONLY)
152
+ end
153
+
154
+ it "overrides the :block_all_mixed_content flag" do
155
+ Configuration.default do |config|
156
+ config.csp = {
157
+ default_src: %w(https:),
158
+ block_all_mixed_content: false
159
+ }
160
+ end
161
+ combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
162
+ csp = ContentSecurityPolicy.new(combined_config)
163
+ expect(csp.value).to eq("default-src https:; block-all-mixed-content")
164
+ end
165
+
166
+ it "raises an error if appending to a OPT_OUT policy" do
167
+ Configuration.default do |config|
168
+ config.csp = OPT_OUT
169
+ end
170
+ expect do
171
+ CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
172
+ end.to raise_error(ContentSecurityPolicyConfigError)
173
+ end
174
+ end
175
+
176
+ describe "#idempotent_additions?" do
177
+ specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
178
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
179
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
180
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
181
+
182
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
183
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
184
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
185
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
186
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
187
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
188
+ end
189
+ end
190
+ end