secure_headers 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

@@ -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