secure_headers 6.0.0.alpha01 → 6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cd74fc8e9c9057255c1e6c75bc78554ec6751459
4
- data.tar.gz: 2f72e782c0800b5bf124d841a0d556d321b1cb5f
3
+ metadata.gz: 6d7392744d486b32bda2b91432d39ddfeee87dfd
4
+ data.tar.gz: 38328982bf71376b9412ed8c7d0652163741bf16
5
5
  SHA512:
6
- metadata.gz: 53ff0d4849c3892fbf4e35270c011d9d6c8da3984958fdc17f9669b32f0d9d39542dc4411323fe93bbf21137f0c8cfe26565a5836548061414153a2abd2dd949
7
- data.tar.gz: 7472a097ae0926655aace8f5f719a4306920b08e4a1b798a588034d51a1446f51f442deb7c3f298d23481d52f2aa1b210ffb40b629671f449f63b7570a7ec2b0
6
+ metadata.gz: e08d9df89db6908d0c8419d0086bf8fb6c7e374c53e0bf774bd74d534094c66ef3cdcbac23e97c03b05d4045ba6878c3f6d8a17877146bdb6f2f0c15b57d1784
7
+ data.tar.gz: 304824374c236d11edc52049732a2ea133bbbdf611f35b2c5309a950125e879fb4016f0f15d7ad6c09310ab4b6080e966fac912147f43ed91989aa15898595a7
data/CHANGELOG.md CHANGED
@@ -1,10 +1,10 @@
1
1
  ## 6.0
2
2
 
3
3
  - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes.
4
- =======
4
+
5
5
  ## 5.0.5
6
6
 
7
- A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
7
+ - A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
8
8
 
9
9
  ## 5.0.4
10
10
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders)
2
2
 
3
- **master represents 5.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md) and [upgrading to 5.x doc](docs/upgrading-to-5-0.md) for instructions on how to upgrade. Bug fixes should go in the 3.x branch for now.
3
+ **master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now.
4
4
 
5
5
  **The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](docs/upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
6
6
 
@@ -77,7 +77,7 @@ SecureHeaders::Configuration.default do |config|
77
77
  preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
78
78
 
79
79
  # directive values: these values will directly translate into source directives
80
- default_src: %w(https: 'self'),
80
+ default_src: %w('none'),
81
81
  base_uri: %w('self'),
82
82
  block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/
83
83
  child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
@@ -43,8 +43,8 @@ Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded
43
43
 
44
44
  Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once.
45
45
 
46
- ## Nonce behavior and console warnings
46
+ ## All user agent sniffing has been removed
47
47
 
48
- Since the first commit, reducing browser console messages was a goal. It led to overly complicated and error-prone UA sniffing. Nowadays, consoles warn on completely legitimate use of features meant to be backwards compatible. So the goal is impossible and the impact is negative, so eliminating code using sniffing is a goal.
48
+ The policy configured is the policy that is delivered in terms of which directives are sent. We still dedup, strip schemes, and look for other optimizations but we will not e.g. conditionally send `frame-src` / `child-src` or apply `nonce`s / `unsafe-inline`.
49
49
 
50
- The first example: we will now send `'unsafe-inline'` along with nonce source expressions. This will generate warnings in some consoles but is 100% valid use and was a design goal of CSP in the early days. The concept of versioning CSP lost out and so we're left with backward compatibility as our only option.
50
+ The primary reason for these per-browser customization was to reduce console warnings. This has lead to many bugs and results inc confusing behavior. Also, console logs are incredibly noisy today and increasingly warn you about perfectly valid things (like sending `X-Frame-Options` and `frame-ancestors` together).
@@ -194,11 +194,11 @@ module SecureHeaders
194
194
  self
195
195
  end
196
196
 
197
- def generate_headers(user_agent)
197
+ def generate_headers
198
198
  headers = {}
199
199
  HEADERABLE_ATTRIBUTES.each do |attr|
200
200
  klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
201
- header_name, value = klass.make_header(instance_variable_get("@#{attr}"), user_agent)
201
+ header_name, value = klass.make_header(instance_variable_get("@#{attr}"))
202
202
  if header_name && value
203
203
  headers[header_name] = value
204
204
  end
@@ -1,19 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "policy_management"
3
3
  require_relative "content_security_policy_config"
4
- require "useragent"
5
4
 
6
5
  module SecureHeaders
7
6
  class ContentSecurityPolicy
8
7
  include PolicyManagement
9
8
 
10
- # constants to be used for version-specific UA sniffing
11
- VERSION_46 = ::UserAgent::Version.new("46")
12
- VERSION_10 = ::UserAgent::Version.new("10")
13
- FALLBACK_VERSION = ::UserAgent::Version.new("0")
14
-
15
- def initialize(config = nil, user_agent = OTHER)
16
- user_agent ||= OTHER
9
+ def initialize(config = nil)
17
10
  @config = if config.is_a?(Hash)
18
11
  if config[:report_only]
19
12
  ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
@@ -26,12 +19,6 @@ module SecureHeaders
26
19
  config
27
20
  end
28
21
 
29
- @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
30
- user_agent
31
- else
32
- UserAgent.parse(user_agent)
33
- end
34
- @frame_src = normalize_child_frame_src
35
22
  @preserve_schemes = @config.preserve_schemes
36
23
  @script_nonce = @config.script_nonce
37
24
  @style_nonce = @config.style_nonce
@@ -56,20 +43,10 @@ module SecureHeaders
56
43
 
57
44
  private
58
45
 
59
- def normalize_child_frame_src
60
- if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
61
- raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
62
- end
63
-
64
- @config.frame_src || @config.child_src
65
- end
66
-
67
46
  # Private: converts the config object into a string representing a policy.
68
47
  # Places default-src at the first directive and report-uri as the last. All
69
48
  # others are presented in alphabetical order.
70
49
  #
71
- # Unsupported directives are filtered based on the user agent.
72
- #
73
50
  # Returns a content security policy header value.
74
51
  def build_value
75
52
  directives.map do |directive_name|
@@ -125,18 +102,7 @@ module SecureHeaders
125
102
  #
126
103
  # Returns a string representing a directive.
127
104
  def build_source_list_directive(directive)
128
- source_list = case directive
129
- when :child_src
130
- if supported_directives.include?(:child_src)
131
- @frame_src
132
- end
133
- when :frame_src
134
- unless supported_directives.include?(:child_src)
135
- @frame_src
136
- end
137
- else
138
- @config.directive_value(directive)
139
- end
105
+ source_list = @config.directive_value(directive)
140
106
 
141
107
  if source_list != OPT_OUT && source_list && source_list.any?
142
108
  normalized_source_list = minify_source_list(directive, source_list)
@@ -219,13 +185,13 @@ module SecureHeaders
219
185
  source_list
220
186
  end
221
187
 
222
- # Private: return the list of directives that are supported by the user agent,
188
+ # Private: return the list of directives,
223
189
  # starting with default-src and ending with report-uri.
224
190
  def directives
225
191
  [
226
192
  DEFAULT_SRC,
227
- BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
228
- REPORT_URI
193
+ BODY_DIRECTIVES,
194
+ REPORT_URI,
229
195
  ].flatten
230
196
  end
231
197
 
@@ -234,25 +200,6 @@ module SecureHeaders
234
200
  source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
235
201
  end
236
202
 
237
- # Private: determine which directives are supported for the given user agent.
238
- #
239
- # Add UA-sniffing special casing here.
240
- #
241
- # Returns an array of symbols representing the directives.
242
- def supported_directives
243
- @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
244
- if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
245
- VARIATIONS["FirefoxTransitional"]
246
- elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
247
- VARIATIONS["SafariTransitional"]
248
- else
249
- VARIATIONS[@parsed_ua.browser]
250
- end
251
- else
252
- VARIATIONS[OTHER]
253
- end
254
- end
255
-
256
203
  def symbol_to_hyphen_case(sym)
257
204
  sym.to_s.tr("_", "-")
258
205
  end
@@ -81,58 +81,12 @@ module SecureHeaders
81
81
  UPGRADE_INSECURE_REQUESTS
82
82
  ].flatten.freeze
83
83
 
84
- EDGE_DIRECTIVES = DIRECTIVES_1_0
85
- SAFARI_DIRECTIVES = DIRECTIVES_1_0
86
- SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
87
-
88
- FIREFOX_UNSUPPORTED_DIRECTIVES = [
89
- BLOCK_ALL_MIXED_CONTENT,
90
- CHILD_SRC,
91
- WORKER_SRC,
92
- PLUGIN_TYPES
93
- ].freeze
94
-
95
- FIREFOX_46_DEPRECATED_DIRECTIVES = [
96
- FRAME_SRC
97
- ].freeze
98
-
99
- FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
100
- BLOCK_ALL_MIXED_CONTENT,
101
- WORKER_SRC,
102
- PLUGIN_TYPES
103
- ].freeze
104
-
105
- FIREFOX_DIRECTIVES = (
106
- DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
107
- ).freeze
108
-
109
- FIREFOX_46_DIRECTIVES = (
110
- DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
111
- ).freeze
112
-
113
- CHROME_DIRECTIVES = (
114
- DIRECTIVES_3_0
115
- ).freeze
116
-
117
84
  ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
118
85
 
119
86
  # Think of default-src and report-uri as the beginning and end respectively,
120
87
  # everything else is in between.
121
88
  BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
122
89
 
123
- VARIATIONS = {
124
- "Chrome" => CHROME_DIRECTIVES,
125
- "Opera" => CHROME_DIRECTIVES,
126
- "Firefox" => FIREFOX_DIRECTIVES,
127
- "FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
128
- "Safari" => SAFARI_DIRECTIVES,
129
- "SafariTransitional" => SAFARI_10_DIRECTIVES,
130
- "Edge" => EDGE_DIRECTIVES,
131
- "Other" => CHROME_DIRECTIVES
132
- }.freeze
133
-
134
- OTHER = "Other".freeze
135
-
136
90
  DIRECTIVE_VALUE_TYPES = {
137
91
  BASE_URI => :source_list,
138
92
  BLOCK_ALL_MIXED_CONTENT => :boolean,
@@ -199,9 +153,9 @@ module SecureHeaders
199
153
  #
200
154
  # Returns a default policy if no configuration is provided, or a
201
155
  # header name and value based on the config.
202
- def make_header(config, user_agent = nil)
156
+ def make_header(config)
203
157
  return if config.nil? || config == OPT_OUT
204
- header = new(config, user_agent)
158
+ header = new(config)
205
159
  [header.name, header.value]
206
160
  end
207
161
 
@@ -303,17 +257,11 @@ module SecureHeaders
303
257
  # Don't set a default if directive has an existing value
304
258
  next if original[directive]
305
259
  if FETCH_SOURCES.include?(directive)
306
- original[directive] = default_for(directive, original)
260
+ original[directive] = original[DEFAULT_SRC]
307
261
  end
308
262
  end
309
263
  end
310
264
 
311
- def default_for(directive, original)
312
- return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
313
- return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
314
- original[DEFAULT_SRC]
315
- end
316
-
317
265
  def source_list?(directive)
318
266
  DIRECTIVE_VALUE_TYPES[directive] == :source_list
319
267
  end
@@ -19,7 +19,7 @@ module SecureHeaders
19
19
  #
20
20
  # Returns an html-safe link tag with the nonce attribute.
21
21
  def nonced_stylesheet_link_tag(*args, &block)
22
- opts = extract_options(args).merge(nonce: content_security_policy_nonce(:style))
22
+ opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
23
23
 
24
24
  stylesheet_link_tag(*args, opts, &block)
25
25
  end
@@ -37,7 +37,7 @@ module SecureHeaders
37
37
  #
38
38
  # Returns an html-safe script tag with the nonce attribute.
39
39
  def nonced_javascript_include_tag(*args, &block)
40
- opts = extract_options(args).merge(nonce: content_security_policy_nonce(:script))
40
+ opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
41
41
 
42
42
  javascript_include_tag(*args, opts, &block)
43
43
  end
@@ -47,7 +47,7 @@ module SecureHeaders
47
47
  #
48
48
  # Returns an html-safe script tag with the nonce attribute.
49
49
  def nonced_javascript_pack_tag(*args, &block)
50
- opts = extract_options(args).merge(nonce: content_security_policy_nonce(:script))
50
+ opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
51
51
 
52
52
  javascript_pack_tag(*args, opts, &block)
53
53
  end
@@ -57,7 +57,7 @@ module SecureHeaders
57
57
  #
58
58
  # Returns an html-safe link tag with the nonce attribute.
59
59
  def nonced_stylesheet_pack_tag(*args, &block)
60
- opts = extract_options(args).merge(nonce: content_security_policy_nonce(:style))
60
+ opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
61
61
 
62
62
  stylesheet_pack_tag(*args, opts, &block)
63
63
  end
@@ -66,7 +66,7 @@ module SecureHeaders
66
66
  # Instructs secure_headers to append a nonce to style/script-src directives.
67
67
  #
68
68
  # Returns a non-html-safe nonce value.
69
- def content_security_policy_nonce(type)
69
+ def _content_security_policy_nonce(type)
70
70
  case type
71
71
  when :script
72
72
  SecureHeaders.content_security_policy_script_nonce(@_request)
@@ -74,13 +74,14 @@ module SecureHeaders
74
74
  SecureHeaders.content_security_policy_style_nonce(@_request)
75
75
  end
76
76
  end
77
+ alias_method :content_security_policy_nonce, :_content_security_policy_nonce
77
78
 
78
79
  def content_security_policy_script_nonce
79
- content_security_policy_nonce(:script)
80
+ _content_security_policy_nonce(:script)
80
81
  end
81
82
 
82
83
  def content_security_policy_style_nonce
83
- content_security_policy_nonce(:style)
84
+ _content_security_policy_nonce(:style)
84
85
  end
85
86
 
86
87
  ##
@@ -152,7 +153,7 @@ module SecureHeaders
152
153
  else
153
154
  content_or_options.html_safe # :'(
154
155
  end
155
- content_tag type, content, options.merge(nonce: content_security_policy_nonce(type))
156
+ content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type))
156
157
  end
157
158
 
158
159
  def extract_options(args)
@@ -15,7 +15,6 @@ require "secure_headers/headers/expect_certificate_transparency"
15
15
  require "secure_headers/middleware"
16
16
  require "secure_headers/railtie"
17
17
  require "secure_headers/view_helper"
18
- require "useragent"
19
18
  require "singleton"
20
19
  require "secure_headers/configuration"
21
20
 
@@ -149,8 +148,7 @@ module SecureHeaders
149
148
  prevent_dup = true
150
149
  config = config_for(request, prevent_dup)
151
150
  config.validate_config!
152
- user_agent = UserAgent.parse(request.user_agent)
153
- headers = config.generate_headers(user_agent)
151
+ headers = config.generate_headers
154
152
 
155
153
  if request.scheme != HTTPS
156
154
  HTTPS_HEADER_CLASSES.each do |klass|
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
  Gem::Specification.new do |gem|
4
4
  gem.name = "secure_headers"
5
- gem.version = "6.0.0.alpha01"
5
+ gem.version = "6.0.0"
6
6
  gem.authors = ["Neil Matatall"]
7
7
  gem.email = ["neil.matatall@gmail.com"]
8
8
  gem.description = "Manages application of security headers with many safe defaults."
@@ -16,5 +16,4 @@ Gem::Specification.new do |gem|
16
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
17
  gem.require_paths = ["lib"]
18
18
  gem.add_development_dependency "rake"
19
- gem.add_dependency "useragent", ">= 0.15.0"
20
19
  end
@@ -116,73 +116,10 @@ module SecureHeaders
116
116
  ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
117
117
  end
118
118
 
119
- it "raises an error when child-src and frame-src are supplied but are not equal" do
120
- expect {
121
- ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
122
- }.to raise_error(ArgumentError)
123
- end
124
-
125
119
  it "supports strict-dynamic" do
126
- csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
120
+ csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456})
127
121
  expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
128
122
  end
129
-
130
- context "browser sniffing" do
131
- let (:complex_opts) do
132
- (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
133
- hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
134
- end.merge({
135
- block_all_mixed_content: true,
136
- upgrade_insecure_requests: true,
137
- script_src: %w(script-src.com),
138
- script_nonce: 123456,
139
- sandbox: %w(allow-forms),
140
- plugin_types: %w(application/pdf)
141
- })
142
- end
143
-
144
- it "does not filter any directives for Chrome" do
145
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
146
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
147
- end
148
-
149
- it "does not filter any directives for Opera" do
150
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
151
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
152
- end
153
-
154
- it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
155
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
156
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
157
- end
158
-
159
- it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
160
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
161
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
162
- end
163
-
164
- it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for Edge" do
165
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
166
- expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
167
- end
168
-
169
- it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for safari" do
170
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
171
- expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
172
- end
173
-
174
- it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, and hash sources for safari 10 and higher" do
175
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
176
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
177
- end
178
-
179
- it "falls back to standard Firefox defaults when the useragent version is not present" do
180
- ua = USER_AGENTS[:firefox].dup
181
- allow(ua).to receive(:version).and_return(nil)
182
- policy = ContentSecurityPolicy.new(complex_opts, ua)
183
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
184
- end
185
- end
186
123
  end
187
124
  end
188
125
  end
@@ -190,7 +190,7 @@ module SecureHeaders
190
190
  report_uri = "https://report-uri.io/asdf"
191
191
  default_policy = Configuration.dup
192
192
  combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri])
193
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
193
+ csp = ContentSecurityPolicy.new(combined_config)
194
194
  expect(csp.value).to include("report-uri #{report_uri}")
195
195
  end
196
196
 
@@ -223,7 +223,7 @@ module SecureHeaders
223
223
  end
224
224
  default_policy = Configuration.dup
225
225
  combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true)
226
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
226
+ csp = ContentSecurityPolicy.new(combined_config)
227
227
  expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
228
228
  end
229
229
 
@@ -97,6 +97,12 @@ TEMPLATE
97
97
  end
98
98
  end
99
99
 
100
+ class MessageWithConflictingMethod < Message
101
+ def content_security_policy_nonce
102
+ "rails-nonce"
103
+ end
104
+ end
105
+
100
106
  module SecureHeaders
101
107
  describe ViewHelpers do
102
108
  let(:app) { lambda { |env| [200, env, "app"] } }
@@ -159,5 +165,27 @@ module SecureHeaders
159
165
  expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
160
166
  end
161
167
  end
168
+
169
+ it "avoids calling content_security_policy_nonce internally" do
170
+ begin
171
+ allow(SecureRandom).to receive(:base64).and_return("abc123")
172
+
173
+ expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8="
174
+ Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"])
175
+ expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc="
176
+ Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"])
177
+
178
+ # render erb that calls out to helpers.
179
+ MessageWithConflictingMethod.new(request).result
180
+ _, env = middleware.call request.env
181
+
182
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
183
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
184
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
185
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
186
+
187
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).not_to match(/rails-nonce/)
188
+ end
189
+ end
162
190
  end
163
191
  end
@@ -127,27 +127,6 @@ module SecureHeaders
127
127
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
128
128
  end
129
129
 
130
- it "produces a UA-specific CSP when overriding (and busting the cache)" do
131
- Configuration.default do |config|
132
- config.csp = {
133
- default_src: %w('self'),
134
- script_src: %w('self'),
135
- child_src: %w('self')
136
- }
137
- end
138
- firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
139
-
140
- # append an unsupported directive
141
- SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(application/pdf)})
142
- # append a supported directive
143
- SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
144
-
145
- hash = SecureHeaders.header_hash_for(firefox_request)
146
-
147
- # child-src is translated to frame-src
148
- expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
149
- end
150
-
151
130
  it "produces a hash of headers with default config" do
152
131
  Configuration.default
153
132
  hash = SecureHeaders.header_hash_for(request)
@@ -197,22 +176,6 @@ module SecureHeaders
197
176
  expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
198
177
  end
199
178
 
200
- it "child-src and frame-src must match" do
201
- Configuration.default do |config|
202
- config.csp = {
203
- default_src: %w('self'),
204
- frame_src: %w(frame_src.com),
205
- script_src: %w('self')
206
- }
207
- end
208
-
209
- SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
210
-
211
- expect {
212
- SecureHeaders.header_hash_for(chrome_request)
213
- }.to raise_error(ArgumentError)
214
- end
215
-
216
179
  it "supports named appends" do
217
180
  Configuration.default do |config|
218
181
  config.csp = {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secure_headers
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0.alpha01
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Matatall
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-13 00:00:00.000000000 Z
11
+ date: 2018-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: useragent
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 0.15.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: 0.15.0
41
27
  description: Manages application of security headers with many safe defaults.
42
28
  email:
43
29
  - neil.matatall@gmail.com
@@ -126,9 +112,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
126
112
  version: '0'
127
113
  required_rubygems_version: !ruby/object:Gem::Requirement
128
114
  requirements:
129
- - - ">"
115
+ - - ">="
130
116
  - !ruby/object:Gem::Version
131
- version: 1.3.1
117
+ version: '0'
132
118
  requirements: []
133
119
  rubyforge_project:
134
120
  rubygems_version: 2.6.13