secure_headers 3.2.0 → 3.7.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +41 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. data/.rspec +1 -0
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +2 -0
  7. data/CHANGELOG.md +140 -1
  8. data/CODE_OF_CONDUCT.md +46 -0
  9. data/CONTRIBUTING.md +41 -0
  10. data/Gemfile +3 -1
  11. data/LICENSE +4 -199
  12. data/README.md +74 -334
  13. data/docs/HPKP.md +17 -0
  14. data/docs/cookies.md +50 -0
  15. data/docs/hashes.md +64 -0
  16. data/docs/named_overrides_and_appends.md +107 -0
  17. data/docs/per_action_configuration.md +105 -0
  18. data/docs/sinatra.md +25 -0
  19. data/lib/secure_headers/configuration.rb +120 -39
  20. data/lib/secure_headers/headers/clear_site_data.rb +54 -0
  21. data/lib/secure_headers/headers/content_security_policy.rb +89 -32
  22. data/lib/secure_headers/headers/content_security_policy_config.rb +161 -0
  23. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  24. data/lib/secure_headers/headers/policy_management.rb +84 -49
  25. data/lib/secure_headers/headers/public_key_pins.rb +1 -1
  26. data/lib/secure_headers/headers/referrer_policy.rb +36 -0
  27. data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
  28. data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
  29. data/lib/secure_headers/headers/x_download_options.rb +1 -1
  30. data/lib/secure_headers/headers/x_frame_options.rb +1 -1
  31. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
  32. data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
  33. data/lib/secure_headers/middleware.rb +6 -0
  34. data/lib/secure_headers/railtie.rb +1 -1
  35. data/lib/secure_headers/view_helper.rb +13 -7
  36. data/lib/secure_headers.rb +138 -55
  37. data/lib/tasks/tasks.rake +5 -4
  38. data/secure_headers.gemspec +2 -2
  39. data/spec/lib/secure_headers/configuration_spec.rb +5 -5
  40. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +86 -0
  41. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +69 -12
  42. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  43. data/spec/lib/secure_headers/headers/policy_management_spec.rb +28 -36
  44. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +72 -0
  45. data/spec/lib/secure_headers/middleware_spec.rb +31 -1
  46. data/spec/lib/secure_headers/view_helpers_spec.rb +19 -7
  47. data/spec/lib/secure_headers_spec.rb +344 -44
  48. data/spec/spec_helper.rb +8 -3
  49. data/upgrading-to-3-0.md +13 -10
  50. metadata +24 -4
@@ -1,33 +1,45 @@
1
1
  require_relative 'policy_management'
2
+ require_relative 'content_security_policy_config'
3
+ require 'useragent'
2
4
 
3
5
  module SecureHeaders
4
- class ContentSecurityPolicyConfigError < StandardError; end
5
6
  class ContentSecurityPolicy
6
7
  include PolicyManagement
7
8
 
9
+ # constants to be used for version-specific UA sniffing
10
+ VERSION_46 = ::UserAgent::Version.new("46")
11
+ VERSION_10 = ::UserAgent::Version.new("10")
12
+ FALLBACK_VERSION = ::UserAgent::Version.new("0")
13
+
8
14
  def initialize(config = nil, user_agent = OTHER)
9
- config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
10
- @config = config
15
+ @config = if config.is_a?(Hash)
16
+ if config[:report_only]
17
+ ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
18
+ else
19
+ ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
20
+ end
21
+ elsif config.nil?
22
+ ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
23
+ else
24
+ config
25
+ end
26
+
11
27
  @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
12
28
  user_agent
13
29
  else
14
30
  UserAgent.parse(user_agent)
15
31
  end
16
- @report_only = @config[:report_only]
17
- @preserve_schemes = @config[:preserve_schemes]
18
- @script_nonce = @config[:script_nonce]
19
- @style_nonce = @config[:style_nonce]
32
+ @frame_src = normalize_child_frame_src
33
+ @preserve_schemes = @config.preserve_schemes
34
+ @script_nonce = @config.script_nonce
35
+ @style_nonce = @config.style_nonce
20
36
  end
21
37
 
22
38
  ##
23
39
  # Returns the name to use for the header. Either "Content-Security-Policy" or
24
40
  # "Content-Security-Policy-Report-Only"
25
41
  def name
26
- if @report_only
27
- REPORT_ONLY
28
- else
29
- HEADER_NAME
30
- end
42
+ @config.class.const_get(:HEADER_NAME)
31
43
  end
32
44
 
33
45
  ##
@@ -42,6 +54,20 @@ module SecureHeaders
42
54
 
43
55
  private
44
56
 
57
+ # frame-src is deprecated, child-src is being implemented. They are
58
+ # very similar and in most cases, the same value can be used for both.
59
+ def normalize_child_frame_src
60
+ if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
61
+ Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
62
+ end
63
+
64
+ if supported_directives.include?(:child_src)
65
+ @config.child_src || @config.frame_src
66
+ else
67
+ @config.frame_src || @config.child_src
68
+ end
69
+ end
70
+
45
71
  # Private: converts the config object into a string representing a policy.
46
72
  # Places default-src at the first directive and report-uri as the last. All
47
73
  # others are presented in alphabetical order.
@@ -53,9 +79,9 @@ module SecureHeaders
53
79
  directives.map do |directive_name|
54
80
  case DIRECTIVE_VALUE_TYPES[directive_name]
55
81
  when :boolean
56
- symbol_to_hyphen_case(directive_name)
82
+ symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
57
83
  when :string
58
- [symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
84
+ [symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ")
59
85
  else
60
86
  build_directive(directive_name)
61
87
  end
@@ -68,11 +94,19 @@ module SecureHeaders
68
94
  #
69
95
  # Returns a string representing a directive.
70
96
  def build_directive(directive)
71
- return if @config[directive].nil?
72
-
73
- source_list = @config[directive].compact
74
- return if source_list.empty?
75
-
97
+ source_list = case directive
98
+ when :child_src
99
+ if supported_directives.include?(:child_src)
100
+ @frame_src
101
+ end
102
+ when :frame_src
103
+ unless supported_directives.include?(:child_src)
104
+ @frame_src
105
+ end
106
+ else
107
+ @config.directive_value(directive)
108
+ end
109
+ return unless source_list && source_list.any?
76
110
  normalized_source_list = minify_source_list(directive, source_list)
77
111
  [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
78
112
  end
@@ -81,16 +115,17 @@ module SecureHeaders
81
115
  # If a directive contains 'none' but has other values, 'none' is ommitted.
82
116
  # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
83
117
  def minify_source_list(directive, source_list)
118
+ source_list = source_list.compact
84
119
  if source_list.include?(STAR)
85
120
  keep_wildcard_sources(source_list)
86
121
  else
87
- populate_nonces!(directive, source_list)
88
- reject_all_values_if_none!(source_list)
122
+ source_list = populate_nonces(directive, source_list)
123
+ source_list = reject_all_values_if_none(source_list)
89
124
 
90
125
  unless directive == REPORT_URI || @preserve_schemes
91
- strip_source_schemes!(source_list)
126
+ source_list = strip_source_schemes(source_list)
92
127
  end
93
- dedup_source_list(source_list).join(" ")
128
+ dedup_source_list(source_list)
94
129
  end
95
130
  end
96
131
 
@@ -100,8 +135,12 @@ module SecureHeaders
100
135
  end
101
136
 
102
137
  # 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
138
+ def reject_all_values_if_none(source_list)
139
+ if source_list.length > 1
140
+ source_list.reject { |value| value == NONE }
141
+ else
142
+ source_list
143
+ end
105
144
  end
106
145
 
107
146
  # Removes duplicates and sources that already match an existing wild card.
@@ -123,12 +162,14 @@ module SecureHeaders
123
162
 
124
163
  # Private: append a nonce to the script/style directories if script_nonce
125
164
  # or style_nonce are provided.
126
- def populate_nonces!(directive, source_list)
165
+ def populate_nonces(directive, source_list)
127
166
  case directive
128
167
  when SCRIPT_SRC
129
168
  append_nonce(source_list, @script_nonce)
130
169
  when STYLE_SRC
131
170
  append_nonce(source_list, @style_nonce)
171
+ else
172
+ source_list
132
173
  end
133
174
  end
134
175
 
@@ -145,30 +186,46 @@ module SecureHeaders
145
186
  source_list << UNSAFE_INLINE
146
187
  end
147
188
  end
189
+
190
+ source_list
148
191
  end
149
192
 
150
193
  # Private: return the list of directives that are supported by the user agent,
151
194
  # starting with default-src and ending with report-uri.
152
195
  def directives
153
- [DEFAULT_SRC,
196
+ [
197
+ DEFAULT_SRC,
154
198
  BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
155
- REPORT_URI].flatten.select { |directive| @config.key?(directive) }
199
+ REPORT_URI
200
+ ].flatten
156
201
  end
157
202
 
158
203
  # Private: Remove scheme from source expressions.
159
- def strip_source_schemes!(source_list)
160
- source_list.map! { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
204
+ def strip_source_schemes(source_list)
205
+ source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
161
206
  end
162
207
 
163
208
  # Private: determine which directives are supported for the given user agent.
164
209
  #
210
+ # Add UA-sniffing special casing here.
211
+ #
165
212
  # Returns an array of symbols representing the directives.
166
213
  def supported_directives
167
- @supported_directives ||= VARIATIONS[@parsed_ua.browser] || VARIATIONS[OTHER]
214
+ @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
215
+ if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
216
+ VARIATIONS["FirefoxTransitional"]
217
+ elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
218
+ VARIATIONS["SafariTransitional"]
219
+ else
220
+ VARIATIONS[@parsed_ua.browser]
221
+ end
222
+ else
223
+ VARIATIONS[OTHER]
224
+ end
168
225
  end
169
226
 
170
227
  def nonces_supported?
171
- @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser)
228
+ @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
172
229
  end
173
230
 
174
231
  def symbol_to_hyphen_case(sym)
@@ -0,0 +1,161 @@
1
+ module SecureHeaders
2
+ module DynamicConfig
3
+ def self.included(base)
4
+ base.send(:attr_writer, :modified)
5
+ base.send(:attr_reader, *base.attrs)
6
+ base.attrs.each do |attr|
7
+ base.send(:define_method, "#{attr}=") do |value|
8
+ if self.class.attrs.include?(attr)
9
+ write_attribute(attr, value)
10
+ else
11
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize(hash)
18
+ @base_uri = nil
19
+ @block_all_mixed_content = nil
20
+ @child_src = nil
21
+ @connect_src = nil
22
+ @default_src = nil
23
+ @font_src = nil
24
+ @form_action = nil
25
+ @frame_ancestors = nil
26
+ @frame_src = nil
27
+ @img_src = nil
28
+ @manifest_src = nil
29
+ @media_src = nil
30
+ @object_src = nil
31
+ @plugin_types = nil
32
+ @preserve_schemes = nil
33
+ @report_only = nil
34
+ @report_uri = nil
35
+ @sandbox = nil
36
+ @script_nonce = nil
37
+ @script_src = nil
38
+ @style_nonce = nil
39
+ @style_src = nil
40
+ @upgrade_insecure_requests = nil
41
+
42
+ from_hash(hash)
43
+ @modified = false
44
+ end
45
+
46
+ def update_directive(directive, value)
47
+ self.send("#{directive}=", value)
48
+ end
49
+
50
+ def directive_value(directive)
51
+ if self.class.attrs.include?(directive)
52
+ self.send(directive)
53
+ end
54
+ end
55
+
56
+ def modified?
57
+ @modified
58
+ end
59
+
60
+ def merge(new_hash)
61
+ ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
62
+ end
63
+
64
+ def merge!(new_hash)
65
+ from_hash(new_hash)
66
+ end
67
+
68
+ def append(new_hash)
69
+ from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash))
70
+ end
71
+
72
+ def to_h
73
+ self.class.attrs.each_with_object({}) do |key, hash|
74
+ value = self.send(key)
75
+ hash[key] = value unless value.nil?
76
+ end
77
+ end
78
+
79
+ def dup
80
+ self.class.new(self.to_h)
81
+ end
82
+
83
+ def opt_out?
84
+ false
85
+ end
86
+
87
+ def ==(o)
88
+ self.class == o.class && self.to_h == o.to_h
89
+ end
90
+
91
+ alias_method :[], :directive_value
92
+ alias_method :[]=, :update_directive
93
+
94
+ private
95
+ def from_hash(hash)
96
+ hash.each_pair do |k, v|
97
+ next if v.nil?
98
+
99
+ if self.class.attrs.include?(k)
100
+ write_attribute(k, v)
101
+ else
102
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
103
+ end
104
+ end
105
+ end
106
+
107
+ def write_attribute(attr, value)
108
+ value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
109
+ attr_variable = "@#{attr}"
110
+ prev_value = self.instance_variable_get(attr_variable)
111
+ self.instance_variable_set(attr_variable, value)
112
+ if prev_value != value
113
+ @modified = true
114
+ end
115
+ end
116
+ end
117
+
118
+ class ContentSecurityPolicyConfigError < StandardError; end
119
+ class ContentSecurityPolicyConfig
120
+ CONFIG_KEY = :csp
121
+ HEADER_NAME = "Content-Security-Policy".freeze
122
+
123
+ ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
124
+ def self.attrs
125
+ ATTRS
126
+ end
127
+
128
+ include DynamicConfig
129
+
130
+ # based on what was suggested in https://github.com/rails/rails/pull/24961/files
131
+ DEFAULT = {
132
+ default_src: %w('self' https:),
133
+ font_src: %w('self' https: data:),
134
+ img_src: %w('self' https: data:),
135
+ object_src: %w('none'),
136
+ script_src: %w(https:),
137
+ style_src: %w('self' https: 'unsafe-inline')
138
+ }
139
+
140
+ def report_only?
141
+ false
142
+ end
143
+
144
+ def make_report_only
145
+ ContentSecurityPolicyReportOnlyConfig.new(self.to_h)
146
+ end
147
+ end
148
+
149
+ class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
150
+ CONFIG_KEY = :csp_report_only
151
+ HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
152
+
153
+ def report_only?
154
+ true
155
+ end
156
+
157
+ def make_report_only
158
+ self
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ExpectCertificateTransparencyConfigError < StandardError; end
4
+
5
+ class ExpectCertificateTransparency
6
+ HEADER_NAME = "Expect-CT".freeze
7
+ CONFIG_KEY = :expect_certificate_transparency
8
+ INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
9
+ INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
10
+ REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
11
+ INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
12
+
13
+ class << self
14
+ # Public: Generate a Expect-CT header.
15
+ #
16
+ # Returns nil if not configured, returns header name and value if
17
+ # configured.
18
+ def make_header(config)
19
+ return if config.nil?
20
+
21
+ header = new(config)
22
+ [HEADER_NAME, header.value]
23
+ end
24
+
25
+ def validate_config!(config)
26
+ return if config.nil? || config == OPT_OUT
27
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
28
+
29
+ unless [true, false, nil].include?(config[:enforce])
30
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
31
+ end
32
+
33
+ if !config[:max_age]
34
+ raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
35
+ elsif config[:max_age].to_s !~ /\A\d+\z/
36
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
37
+ end
38
+ end
39
+ end
40
+
41
+ def initialize(config)
42
+ @enforced = config.fetch(:enforce, nil)
43
+ @max_age = config.fetch(:max_age, nil)
44
+ @report_uri = config.fetch(:report_uri, nil)
45
+ end
46
+
47
+ def value
48
+ header_value = [
49
+ enforced_directive,
50
+ max_age_directive,
51
+ report_uri_directive
52
+ ].compact.join("; ").strip
53
+ end
54
+
55
+ def enforced_directive
56
+ # Unfortunately `if @enforced` isn't enough here in case someone
57
+ # passes in a random string so let's be specific with it to prevent
58
+ # accidental enforcement.
59
+ "enforce" if @enforced == true
60
+ end
61
+
62
+ def max_age_directive
63
+ "max-age=#{@max_age}" if @max_age
64
+ end
65
+
66
+ def report_uri_directive
67
+ "report-uri=\"#{@report_uri}\"" if @report_uri
68
+ end
69
+ end
70
+ end
@@ -7,9 +7,6 @@ module SecureHeaders
7
7
  MODERN_BROWSERS = %w(Chrome Opera Firefox)
8
8
  DEFAULT_VALUE = "default-src https:".freeze
9
9
  DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
10
- HEADER_NAME = "Content-Security-Policy".freeze
11
- REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
12
- HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
13
10
  DATA_PROTOCOL = "data:".freeze
14
11
  BLOB_PROTOCOL = "blob:".freeze
15
12
  SELF = "'self'".freeze
@@ -17,6 +14,7 @@ module SecureHeaders
17
14
  STAR = "*".freeze
18
15
  UNSAFE_INLINE = "'unsafe-inline'".freeze
19
16
  UNSAFE_EVAL = "'unsafe-eval'".freeze
17
+ STRICT_DYNAMIC = "'strict-dynamic'".freeze
20
18
 
21
19
  # leftover deprecated values that will be in common use upon upgrading.
22
20
  DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
@@ -53,16 +51,6 @@ module SecureHeaders
53
51
  FRAME_ANCESTORS = :frame_ancestors
54
52
  PLUGIN_TYPES = :plugin_types
55
53
 
56
- # These are directives that do not inherit the default-src value. This is
57
- # useful when calling #combine_policies.
58
- NON_FETCH_SOURCES = [
59
- BASE_URI,
60
- FORM_ACTION,
61
- FRAME_ANCESTORS,
62
- PLUGIN_TYPES,
63
- REPORT_URI
64
- ]
65
-
66
54
  DIRECTIVES_2_0 = [
67
55
  DIRECTIVES_1_0,
68
56
  BASE_URI,
@@ -74,24 +62,19 @@ module SecureHeaders
74
62
 
75
63
  # All the directives currently under consideration for CSP level 3.
76
64
  # https://w3c.github.io/webappsec/specs/CSP2/
65
+ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
77
66
  MANIFEST_SRC = :manifest_src
78
- REFLECTED_XSS = :reflected_xss
67
+ UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
79
68
  DIRECTIVES_3_0 = [
80
69
  DIRECTIVES_2_0,
81
- MANIFEST_SRC,
82
- REFLECTED_XSS
83
- ].flatten.freeze
84
-
85
- # All the directives that are not currently in a formal spec, but have
86
- # been implemented somewhere.
87
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
88
- UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
89
- DIRECTIVES_DRAFT = [
90
70
  BLOCK_ALL_MIXED_CONTENT,
71
+ MANIFEST_SRC,
91
72
  UPGRADE_INSECURE_REQUESTS
92
- ].freeze
73
+ ].flatten.freeze
93
74
 
75
+ EDGE_DIRECTIVES = DIRECTIVES_1_0
94
76
  SAFARI_DIRECTIVES = DIRECTIVES_1_0
77
+ SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
95
78
 
96
79
  FIREFOX_UNSUPPORTED_DIRECTIVES = [
97
80
  BLOCK_ALL_MIXED_CONTENT,
@@ -99,25 +82,53 @@ module SecureHeaders
99
82
  PLUGIN_TYPES
100
83
  ].freeze
101
84
 
85
+ FIREFOX_46_DEPRECATED_DIRECTIVES = [
86
+ FRAME_SRC
87
+ ].freeze
88
+
89
+ FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
90
+ BLOCK_ALL_MIXED_CONTENT,
91
+ PLUGIN_TYPES
92
+ ].freeze
93
+
102
94
  FIREFOX_DIRECTIVES = (
103
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
95
+ DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
96
+ ).freeze
97
+
98
+ FIREFOX_46_DIRECTIVES = (
99
+ DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
104
100
  ).freeze
105
101
 
106
102
  CHROME_DIRECTIVES = (
107
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
103
+ DIRECTIVES_3_0
108
104
  ).freeze
109
105
 
110
- ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
106
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
111
107
 
112
108
  # Think of default-src and report-uri as the beginning and end respectively,
113
109
  # everything else is in between.
114
110
  BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
115
111
 
112
+ # These are directives that do not inherit the default-src value. This is
113
+ # useful when calling #combine_policies.
114
+ NON_FETCH_SOURCES = [
115
+ BASE_URI,
116
+ FORM_ACTION,
117
+ FRAME_ANCESTORS,
118
+ PLUGIN_TYPES,
119
+ REPORT_URI
120
+ ]
121
+
122
+ FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES
123
+
116
124
  VARIATIONS = {
117
125
  "Chrome" => CHROME_DIRECTIVES,
118
126
  "Opera" => CHROME_DIRECTIVES,
119
127
  "Firefox" => FIREFOX_DIRECTIVES,
128
+ "FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
120
129
  "Safari" => SAFARI_DIRECTIVES,
130
+ "SafariTransitional" => SAFARI_10_DIRECTIVES,
131
+ "Edge" => EDGE_DIRECTIVES,
121
132
  "Other" => CHROME_DIRECTIVES
122
133
  }.freeze
123
134
 
@@ -138,15 +149,14 @@ module SecureHeaders
138
149
  MEDIA_SRC => :source_list,
139
150
  OBJECT_SRC => :source_list,
140
151
  PLUGIN_TYPES => :source_list,
141
- REFLECTED_XSS => :string,
142
152
  REPORT_URI => :source_list,
143
- SANDBOX => :string,
153
+ SANDBOX => :source_list,
144
154
  SCRIPT_SRC => :source_list,
145
155
  STYLE_SRC => :source_list,
146
156
  UPGRADE_INSECURE_REQUESTS => :boolean
147
157
  }.freeze
148
158
 
149
- CONFIG_KEY = :csp
159
+
150
160
  STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
151
161
  HTTP_SCHEME_REGEX = %r{\Ahttps?://}
152
162
 
@@ -163,6 +173,11 @@ module SecureHeaders
163
173
  :preserve_schemes
164
174
  ].freeze
165
175
 
176
+ NONCES = [
177
+ :script_nonce,
178
+ :style_nonce
179
+ ].freeze
180
+
166
181
  module ClassMethods
167
182
  # Public: generate a header name, value array that is user-agent-aware.
168
183
  #
@@ -178,9 +193,11 @@ module SecureHeaders
178
193
  # Does not validate the invididual values of the source expression (e.g.
179
194
  # script_src => h*t*t*p: will not raise an exception)
180
195
  def validate_config!(config)
181
- return if config.nil? || config == OPT_OUT
182
- raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
183
- config.each do |key, value|
196
+ return if config.nil? || config.opt_out?
197
+ raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
198
+ ContentSecurityPolicyConfig.attrs.each do |key|
199
+ value = config.directive_value(key)
200
+ next unless value
184
201
  if META_CONFIGS.include?(key)
185
202
  raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
186
203
  else
@@ -189,16 +206,13 @@ module SecureHeaders
189
206
  end
190
207
  end
191
208
 
192
- # Public: determine if merging +additions+ will cause a change to the
193
- # actual value of the config.
209
+ # Public: check if a user agent supports CSP nonces
194
210
  #
195
- # e.g. config = { script_src: %w(example.org google.com)} and
196
- # additions = { script_src: %w(google.com)} then idempotent_additions? would return
197
- # because google.com is already in the config.
198
- def idempotent_additions?(config, additions)
199
- return true if config == OPT_OUT && additions == OPT_OUT
200
- return false if config == OPT_OUT
201
- config == combine_policies(config, additions)
211
+ # user_agent - a String or a UserAgent object
212
+ def nonces_supported?(user_agent)
213
+ user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
214
+ MODERN_BROWSERS.include?(user_agent.browser) ||
215
+ user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
202
216
  end
203
217
 
204
218
  # Public: combine the values from two different configs.
@@ -215,7 +229,7 @@ module SecureHeaders
215
229
  # 3. if a value in additions does exist in the original config, the two
216
230
  # values are joined.
217
231
  def combine_policies(original, additions)
218
- if original == OPT_OUT
232
+ if original == {}
219
233
  raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
220
234
  end
221
235
 
@@ -251,9 +265,30 @@ module SecureHeaders
251
265
  # copy the default-src value to the original config. This modifies the original hash.
252
266
  def populate_fetch_source_with_default!(original, additions)
253
267
  # in case we would be appending to an empty directive, fill it with the default-src value
254
- additions.keys.each do |directive|
255
- unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive)
256
- original[directive] = original[:default_src]
268
+ additions.each_key do |directive|
269
+ if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
270
+ if nonce_added?(original, additions)
271
+ inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
272
+ unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
273
+ original[inferred_directive] = default_for(directive, original)
274
+ end
275
+ else
276
+ original[directive] = default_for(directive, original)
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+ def default_for(directive, original)
283
+ return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
284
+ return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
285
+ original[DEFAULT_SRC]
286
+ end
287
+
288
+ def nonce_added?(original, additions)
289
+ [:script_nonce, :style_nonce].each do |nonce|
290
+ if additions[nonce] && !original[nonce]
291
+ return true
257
292
  end
258
293
  end
259
294
  end
@@ -305,9 +340,9 @@ module SecureHeaders
305
340
  end
306
341
 
307
342
  def ensure_valid_sources!(directive, source_expression)
308
- source_expression.each do |source_expression|
309
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
310
- raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
343
+ source_expression.each do |expression|
344
+ if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
345
+ raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
311
346
  end
312
347
  end
313
348
  end