secure_headers 3.0.3 → 3.2.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.
@@ -1,8 +1,11 @@
1
+ require 'yaml'
2
+
1
3
  module SecureHeaders
2
4
  class Configuration
3
5
  DEFAULT_CONFIG = :default
4
6
  NOOP_CONFIGURATION = "secure_headers_noop_config"
5
7
  class NotYetConfiguredError < StandardError; end
8
+ class IllegalPolicyModificationError < StandardError; end
6
9
  class << self
7
10
  # Public: Set the global default configuration.
8
11
  #
@@ -23,12 +26,12 @@ module SecureHeaders
23
26
  # if no value is supplied.
24
27
  #
25
28
  # Returns: the newly created config
26
- def override(name, base = DEFAULT_CONFIG)
29
+ def override(name, base = DEFAULT_CONFIG, &block)
27
30
  unless get(base)
28
31
  raise NotYetConfiguredError, "#{base} policy not yet supplied"
29
32
  end
30
33
  override = @configurations[base].dup
31
- yield(override)
34
+ override.instance_eval &block if block_given?
32
35
  add_configuration(name, override)
33
36
  end
34
37
 
@@ -43,18 +46,6 @@ module SecureHeaders
43
46
  @configurations[name]
44
47
  end
45
48
 
46
- # Public: perform a basic deep dup. The shallow copy provided by dup/clone
47
- # can lead to modifying parent objects.
48
- def deep_copy(config)
49
- config.each_with_object({}) do |(key, value), hash|
50
- hash[key] = if value.is_a?(Array)
51
- value.dup
52
- else
53
- value
54
- end
55
- end
56
- end
57
-
58
49
  private
59
50
 
60
51
  # Private: add a valid configuration to the global set of named configs.
@@ -82,20 +73,51 @@ module SecureHeaders
82
73
  ALL_HEADER_CLASSES.each do |klass|
83
74
  config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
84
75
  end
76
+ config.dynamic_csp = OPT_OUT
85
77
  end
86
78
 
87
79
  add_configuration(NOOP_CONFIGURATION, noop_config)
88
80
  end
81
+
82
+ # Public: perform a basic deep dup. The shallow copy provided by dup/clone
83
+ # can lead to modifying parent objects.
84
+ def deep_copy(config)
85
+ config.each_with_object({}) do |(key, value), hash|
86
+ hash[key] = if value.is_a?(Array)
87
+ value.dup
88
+ else
89
+ value
90
+ end
91
+ end
92
+ end
93
+
94
+ # Private: convenience method purely DRY things up. The value may not be a
95
+ # hash (e.g. OPT_OUT, nil)
96
+ def deep_copy_if_hash(value)
97
+ if value.is_a?(Hash)
98
+ deep_copy(value)
99
+ else
100
+ value
101
+ end
102
+ end
89
103
  end
90
104
 
91
- attr_accessor :hsts, :x_frame_options, :x_content_type_options,
92
- :x_xss_protection, :csp, :x_download_options, :x_permitted_cross_domain_policies,
93
- :hpkp
94
- attr_reader :cached_headers
105
+ attr_writer :hsts, :x_frame_options, :x_content_type_options,
106
+ :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
107
+ :hpkp, :dynamic_csp, :cookies
108
+
109
+ attr_reader :cached_headers, :csp, :dynamic_csp, :cookies
110
+
111
+ HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
112
+ if File.exists?(HASH_CONFIG_FILE)
113
+ config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
114
+ @script_hashes = config["scripts"]
115
+ @style_hashes = config["styles"]
116
+ end
95
117
 
96
118
  def initialize(&block)
97
119
  self.hpkp = OPT_OUT
98
- self.csp = self.class.deep_copy(CSP::DEFAULT_CONFIG)
120
+ self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
99
121
  instance_eval &block if block_given?
100
122
  end
101
123
 
@@ -104,33 +126,45 @@ module SecureHeaders
104
126
  # Returns a deep-dup'd copy of this configuration.
105
127
  def dup
106
128
  copy = self.class.new
107
- copy.hsts = hsts
108
- copy.x_frame_options = x_frame_options
109
- copy.x_content_type_options = x_content_type_options
110
- copy.x_xss_protection = x_xss_protection
111
- copy.x_download_options = x_download_options
112
- copy.x_permitted_cross_domain_policies = x_permitted_cross_domain_policies
113
- copy.csp = if csp.is_a?(Hash)
114
- self.class.deep_copy(csp)
115
- else
116
- csp
129
+ copy.cookies = @cookies
130
+ copy.csp = self.class.send(:deep_copy_if_hash, @csp)
131
+ copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
132
+ copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
133
+ copy.x_content_type_options = @x_content_type_options
134
+ copy.hsts = @hsts
135
+ copy.x_frame_options = @x_frame_options
136
+ copy.x_xss_protection = @x_xss_protection
137
+ copy.x_download_options = @x_download_options
138
+ copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
139
+ copy.hpkp = @hpkp
140
+ copy
141
+ end
142
+
143
+ def opt_out(header)
144
+ send("#{header}=", OPT_OUT)
145
+ if header == CSP::CONFIG_KEY
146
+ dynamic_csp = OPT_OUT
117
147
  end
148
+ self.cached_headers.delete(header)
149
+ end
150
+
151
+ def update_x_frame_options(value)
152
+ @x_frame_options = value
153
+ self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
154
+ end
118
155
 
119
- copy.hpkp = if hpkp.is_a?(Hash)
120
- self.class.deep_copy(hpkp)
121
- else
122
- hpkp
156
+ # Public: generated cached headers for a specific user agent.
157
+ def rebuild_csp_header_cache!(user_agent)
158
+ self.cached_headers[CSP::CONFIG_KEY] = {}
159
+ unless current_csp == OPT_OUT
160
+ user_agent = UserAgent.parse(user_agent)
161
+ variation = CSP.ua_to_variation(user_agent)
162
+ self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
123
163
  end
124
- copy
125
164
  end
126
165
 
127
- # Public: Retrieve a config based on the CONFIG_KEY for a class
128
- #
129
- # Returns the value if available, and returns a dup of any hash values.
130
- def fetch(key)
131
- config = send(key)
132
- config = self.class.deep_copy(config) if config.is_a?(Hash)
133
- config
166
+ def current_csp
167
+ @dynamic_csp || @csp
134
168
  end
135
169
 
136
170
  # Public: validates all configurations values.
@@ -139,16 +173,38 @@ module SecureHeaders
139
173
  #
140
174
  # Returns nothing
141
175
  def validate_config!
142
- StrictTransportSecurity.validate_config!(hsts)
143
- ContentSecurityPolicy.validate_config!(csp)
144
- XFrameOptions.validate_config!(x_frame_options)
145
- XContentTypeOptions.validate_config!(x_content_type_options)
146
- XXssProtection.validate_config!(x_xss_protection)
147
- XDownloadOptions.validate_config!(x_download_options)
148
- XPermittedCrossDomainPolicies.validate_config!(x_permitted_cross_domain_policies)
149
- PublicKeyPins.validate_config!(hpkp)
176
+ StrictTransportSecurity.validate_config!(@hsts)
177
+ ContentSecurityPolicy.validate_config!(@csp)
178
+ XFrameOptions.validate_config!(@x_frame_options)
179
+ XContentTypeOptions.validate_config!(@x_content_type_options)
180
+ XXssProtection.validate_config!(@x_xss_protection)
181
+ XDownloadOptions.validate_config!(@x_download_options)
182
+ XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
183
+ PublicKeyPins.validate_config!(@hpkp)
184
+ Cookie.validate_config!(@cookies)
150
185
  end
151
186
 
187
+ def secure_cookies=(secure_cookies)
188
+ Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead."
189
+ @cookies = (@cookies || {}).merge(secure: secure_cookies)
190
+ end
191
+
192
+ protected
193
+
194
+ def csp=(new_csp)
195
+ if self.dynamic_csp
196
+ raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
197
+ end
198
+
199
+ @csp = new_csp
200
+ end
201
+
202
+ def cached_headers=(headers)
203
+ @cached_headers = headers
204
+ end
205
+
206
+ private
207
+
152
208
  # Public: Precompute the header names and values for this configuraiton.
153
209
  # Ensures that headers generated at configure time, not on demand.
154
210
  #
@@ -156,7 +212,7 @@ module SecureHeaders
156
212
  def cache_headers!
157
213
  # generate defaults for the "easy" headers
158
214
  headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash|
159
- config = fetch(klass::CONFIG_KEY)
215
+ config = instance_variable_get("@#{klass::CONFIG_KEY}")
160
216
  unless config == OPT_OUT
161
217
  hash[klass::CONFIG_KEY] = klass.make_header(config).freeze
162
218
  end
@@ -165,7 +221,7 @@ module SecureHeaders
165
221
  generate_csp_headers(headers)
166
222
 
167
223
  headers.freeze
168
- @cached_headers = headers
224
+ self.cached_headers = headers
169
225
  end
170
226
 
171
227
  # Private: adds CSP headers for each variation of CSP support.
@@ -175,11 +231,10 @@ module SecureHeaders
175
231
  #
176
232
  # Returns nothing
177
233
  def generate_csp_headers(headers)
178
- unless csp == OPT_OUT
234
+ unless @csp == OPT_OUT
179
235
  headers[CSP::CONFIG_KEY] = {}
180
-
236
+ csp_config = self.current_csp
181
237
  CSP::VARIATIONS.each do |name, _|
182
- csp_config = fetch(CSP::CONFIG_KEY)
183
238
  csp = CSP.make_header(csp_config, UserAgent.parse(name))
184
239
  headers[CSP::CONFIG_KEY][name] = csp.freeze
185
240
  end
@@ -0,0 +1,10 @@
1
+ require 'base64'
2
+
3
+ module SecureHeaders
4
+ module HashHelper
5
+ def hash_source(inline_script, digest = :SHA256)
6
+ base64_hashed_content = Base64.encode64(Digest.const_get(digest).digest(inline_script)).chomp
7
+ "'#{digest.to_s.downcase}-#{base64_hashed_content}'"
8
+ end
9
+ end
10
+ end
@@ -1,296 +1,9 @@
1
- require 'uri'
2
- require 'base64'
3
- require 'securerandom'
4
- require 'json'
1
+ require_relative 'policy_management'
5
2
 
6
3
  module SecureHeaders
7
4
  class ContentSecurityPolicyConfigError < StandardError; end
8
5
  class ContentSecurityPolicy
9
- MODERN_BROWSERS = %w(Chrome Opera Firefox)
10
- DEFAULT_VALUE = "default-src https:".freeze
11
- DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
12
- HEADER_NAME = "Content-Security-Policy".freeze
13
- REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
14
- HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
15
- DATA_PROTOCOL = "data:".freeze
16
- BLOB_PROTOCOL = "blob:".freeze
17
- SELF = "'self'".freeze
18
- NONE = "'none'".freeze
19
- STAR = "*".freeze
20
- UNSAFE_INLINE = "'unsafe-inline'".freeze
21
- UNSAFE_EVAL = "'unsafe-eval'".freeze
22
-
23
- # leftover deprecated values that will be in common use upon upgrading.
24
- DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
25
-
26
- DEFAULT_SRC = :default_src
27
- CONNECT_SRC = :connect_src
28
- FONT_SRC = :font_src
29
- FRAME_SRC = :frame_src
30
- IMG_SRC = :img_src
31
- MEDIA_SRC = :media_src
32
- OBJECT_SRC = :object_src
33
- SANDBOX = :sandbox
34
- SCRIPT_SRC = :script_src
35
- STYLE_SRC = :style_src
36
- REPORT_URI = :report_uri
37
-
38
- DIRECTIVES_1_0 = [
39
- DEFAULT_SRC,
40
- CONNECT_SRC,
41
- FONT_SRC,
42
- FRAME_SRC,
43
- IMG_SRC,
44
- MEDIA_SRC,
45
- OBJECT_SRC,
46
- SANDBOX,
47
- SCRIPT_SRC,
48
- STYLE_SRC,
49
- REPORT_URI
50
- ].freeze
51
-
52
- BASE_URI = :base_uri
53
- CHILD_SRC = :child_src
54
- FORM_ACTION = :form_action
55
- FRAME_ANCESTORS = :frame_ancestors
56
- PLUGIN_TYPES = :plugin_types
57
-
58
- # These are directives that do not inherit the default-src value. This is
59
- # useful when calling #combine_policies.
60
- NON_DEFAULT_SOURCES = [
61
- BASE_URI,
62
- FORM_ACTION,
63
- FRAME_ANCESTORS,
64
- PLUGIN_TYPES,
65
- REPORT_URI
66
- ]
67
-
68
- DIRECTIVES_2_0 = [
69
- DIRECTIVES_1_0,
70
- BASE_URI,
71
- CHILD_SRC,
72
- FORM_ACTION,
73
- FRAME_ANCESTORS,
74
- PLUGIN_TYPES
75
- ].flatten.freeze
76
-
77
- # All the directives currently under consideration for CSP level 3.
78
- # https://w3c.github.io/webappsec/specs/CSP2/
79
- MANIFEST_SRC = :manifest_src
80
- REFLECTED_XSS = :reflected_xss
81
- DIRECTIVES_3_0 = [
82
- DIRECTIVES_2_0,
83
- MANIFEST_SRC,
84
- REFLECTED_XSS
85
- ].flatten.freeze
86
-
87
- # All the directives that are not currently in a formal spec, but have
88
- # been implemented somewhere.
89
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
90
- UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
91
- DIRECTIVES_DRAFT = [
92
- BLOCK_ALL_MIXED_CONTENT,
93
- UPGRADE_INSECURE_REQUESTS
94
- ].freeze
95
-
96
- SAFARI_DIRECTIVES = DIRECTIVES_1_0
97
-
98
- FIREFOX_UNSUPPORTED_DIRECTIVES = [
99
- BLOCK_ALL_MIXED_CONTENT,
100
- CHILD_SRC,
101
- PLUGIN_TYPES
102
- ].freeze
103
-
104
- FIREFOX_DIRECTIVES = (
105
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
106
- ).freeze
107
-
108
- CHROME_DIRECTIVES = (
109
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
110
- ).freeze
111
-
112
- ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
113
-
114
- # Think of default-src and report-uri as the beginning and end respectively,
115
- # everything else is in between.
116
- BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
117
-
118
- VARIATIONS = {
119
- "Chrome" => CHROME_DIRECTIVES,
120
- "Opera" => CHROME_DIRECTIVES,
121
- "Firefox" => FIREFOX_DIRECTIVES,
122
- "Safari" => SAFARI_DIRECTIVES,
123
- "Other" => CHROME_DIRECTIVES
124
- }.freeze
125
-
126
- OTHER = "Other".freeze
127
-
128
- DIRECTIVE_VALUE_TYPES = {
129
- BASE_URI => :source_list,
130
- BLOCK_ALL_MIXED_CONTENT => :boolean,
131
- CHILD_SRC => :source_list,
132
- CONNECT_SRC => :source_list,
133
- DEFAULT_SRC => :source_list,
134
- FONT_SRC => :source_list,
135
- FORM_ACTION => :source_list,
136
- FRAME_ANCESTORS => :source_list,
137
- FRAME_SRC => :source_list,
138
- IMG_SRC => :source_list,
139
- MANIFEST_SRC => :source_list,
140
- MEDIA_SRC => :source_list,
141
- OBJECT_SRC => :source_list,
142
- PLUGIN_TYPES => :source_list,
143
- REFLECTED_XSS => :string,
144
- REPORT_URI => :source_list,
145
- SANDBOX => :string,
146
- SCRIPT_SRC => :source_list,
147
- STYLE_SRC => :source_list,
148
- UPGRADE_INSECURE_REQUESTS => :boolean
149
- }.freeze
150
-
151
- CONFIG_KEY = :csp
152
- STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
153
- HTTP_SCHEME_REGEX = %r{\Ahttps?://}
154
-
155
- WILDCARD_SOURCES = [
156
- UNSAFE_EVAL,
157
- UNSAFE_INLINE,
158
- STAR,
159
- DATA_PROTOCOL,
160
- BLOB_PROTOCOL
161
- ].freeze
162
-
163
- META_CONFIGS = [
164
- :report_only,
165
- :preserve_schemes
166
- ].freeze
167
-
168
- class << self
169
- # Public: generate a header name, value array that is user-agent-aware.
170
- #
171
- # Returns a default policy if no configuration is provided, or a
172
- # header name and value based on the config.
173
- def make_header(config, user_agent)
174
- header = new(config, user_agent)
175
- [header.name, header.value]
176
- end
177
-
178
- # Public: Validates each source expression.
179
- #
180
- # Does not validate the invididual values of the source expression (e.g.
181
- # script_src => h*t*t*p: will not raise an exception)
182
- def validate_config!(config)
183
- return if config.nil? || config == OPT_OUT
184
- raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
185
- config.each do |key, value|
186
- if META_CONFIGS.include?(key)
187
- raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
188
- else
189
- validate_directive!(key, value)
190
- end
191
- end
192
- end
193
-
194
- # Public: determine if merging +additions+ will cause a change to the
195
- # actual value of the config.
196
- #
197
- # e.g. config = { script_src: %w(example.org google.com)} and
198
- # additions = { script_src: %w(google.com)} then idempotent_additions? would return
199
- # because google.com is already in the config.
200
- def idempotent_additions?(config, additions)
201
- return false if config == OPT_OUT
202
- config.to_s == combine_policies(config, additions).to_s
203
- end
204
-
205
- # Public: combine the values from two different configs.
206
- #
207
- # original - the main config
208
- # additions - values to be merged in
209
- #
210
- # raises an error if the original config is OPT_OUT
211
- #
212
- # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests),
213
- # additions will overwrite the original value.
214
- # 2. if a value in additions does not exist in the original config, the
215
- # default-src value is included to match original behavior.
216
- # 3. if a value in additions does exist in the original config, the two
217
- # values are joined.
218
- def combine_policies(original, additions)
219
- if original == OPT_OUT
220
- raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
221
- end
222
-
223
- original = original.dup if original.frozen?
224
-
225
- # in case we would be appending to an empty directive, fill it with the default-src value
226
- additions.keys.each do |directive|
227
- unless original[directive] || !source_list?(directive) || NON_DEFAULT_SOURCES.include?(directive)
228
- original[directive] = original[:default_src]
229
- end
230
- end
231
-
232
- # merge the two hashes. combine (instead of overwrite) the array values
233
- # when each hash contains a value for a given key.
234
- original.merge(additions) do |directive, lhs, rhs|
235
- if source_list?(directive)
236
- (lhs.to_a + rhs.to_a).compact.uniq
237
- else
238
- rhs
239
- end
240
- end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
241
- end
242
-
243
- private
244
-
245
- def source_list?(directive)
246
- DIRECTIVE_VALUE_TYPES[directive] == :source_list
247
- end
248
-
249
- # Private: Validates that the configuration has a valid type, or that it is a valid
250
- # source expression.
251
- def validate_directive!(key, value)
252
- case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key]
253
- when :boolean
254
- unless boolean?(value)
255
- raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value")
256
- end
257
- when :string
258
- unless value.is_a?(String)
259
- raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value")
260
- end
261
- else
262
- validate_source_expression!(key, value)
263
- end
264
- end
265
-
266
- # Private: validates that a source expression:
267
- # 1. has a valid name
268
- # 2. is an array of strings
269
- # 3. does not contain any depreated, now invalid values (inline, eval, self, none)
270
- #
271
- # Does not validate the invididual values of the source expression (e.g.
272
- # script_src => h*t*t*p: will not raise an exception)
273
- def validate_source_expression!(key, value)
274
- unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key)
275
- raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}")
276
- end
277
-
278
- unless value.is_a?(Array) && value.compact.all? { |v| v.is_a?(String) }
279
- raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings")
280
- end
281
-
282
- value.each do |source_expression|
283
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
284
- raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
285
- end
286
- end
287
- end
288
-
289
- def boolean?(value)
290
- value.is_a?(TrueClass) || value.is_a?(FalseClass)
291
- end
292
- end
293
-
6
+ include PolicyManagement
294
7
 
295
8
  def initialize(config = nil, user_agent = OTHER)
296
9
  config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
@@ -350,36 +63,45 @@ module SecureHeaders
350
63
  end
351
64
 
352
65
  # Private: builds a string that represents one directive in a minified form.
353
- # If a directive contains *, all other values are omitted.
354
- # If a directive contains 'none' but has other values, 'none' is ommitted.
355
- # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
356
66
  #
357
67
  # directive_name - a symbol representing the various ALL_DIRECTIVES
358
68
  #
359
69
  # Returns a string representing a directive.
360
- def build_directive(directive_name)
361
- return if @config[directive_name].nil?
70
+ def build_directive(directive)
71
+ return if @config[directive].nil?
362
72
 
363
- source_list = @config[directive_name].compact
73
+ source_list = @config[directive].compact
364
74
  return if source_list.empty?
365
75
 
366
- value = if source_list.include?(STAR)
367
- # Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
368
- source_list.select { |value| WILDCARD_SOURCES.include?(value) }
369
- else
370
- populate_nonces(directive_name, source_list)
76
+ normalized_source_list = minify_source_list(directive, source_list)
77
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
78
+ end
371
79
 
372
- # Discard any 'none' values if more directives are supplied since none may override values.
373
- source_list.reject! { |value| value == NONE } if source_list.length > 1
80
+ # If a directive contains *, all other values are omitted.
81
+ # If a directive contains 'none' but has other values, 'none' is ommitted.
82
+ # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
83
+ def minify_source_list(directive, source_list)
84
+ if source_list.include?(STAR)
85
+ keep_wildcard_sources(source_list)
86
+ else
87
+ populate_nonces!(directive, source_list)
88
+ reject_all_values_if_none!(source_list)
374
89
 
375
- # remove schemes and dedup source expressions
376
- unless directive_name == REPORT_URI || @preserve_schemes
377
- source_list = strip_source_schemes(source_list)
90
+ unless directive == REPORT_URI || @preserve_schemes
91
+ strip_source_schemes!(source_list)
378
92
  end
379
93
  dedup_source_list(source_list).join(" ")
380
94
  end
95
+ end
96
+
97
+ # Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
98
+ def keep_wildcard_sources(source_list)
99
+ source_list.select { |value| WILDCARD_SOURCES.include?(value) }
100
+ end
381
101
 
382
- [symbol_to_hyphen_case(directive_name), value].join(" ")
102
+ # Discard any 'none' values if more directives are supplied since none may override values.
103
+ def reject_all_values_if_none!(source_list)
104
+ source_list.reject! { |value| value == NONE } if source_list.length > 1
383
105
  end
384
106
 
385
107
  # Removes duplicates and sources that already match an existing wild card.
@@ -401,7 +123,7 @@ module SecureHeaders
401
123
 
402
124
  # Private: append a nonce to the script/style directories if script_nonce
403
125
  # or style_nonce are provided.
404
- def populate_nonces(directive, source_list)
126
+ def populate_nonces!(directive, source_list)
405
127
  case directive
406
128
  when SCRIPT_SRC
407
129
  append_nonce(source_list, @script_nonce)
@@ -434,8 +156,8 @@ module SecureHeaders
434
156
  end
435
157
 
436
158
  # Private: Remove scheme from source expressions.
437
- def strip_source_schemes(source_list)
438
- source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
159
+ def strip_source_schemes!(source_list)
160
+ source_list.map! { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
439
161
  end
440
162
 
441
163
  # Private: determine which directives are supported for the given user agent.