secure_headers 3.2.0 → 4.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.
Files changed (65) 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 +2 -0
  5. data/.rubocop.yml +3 -0
  6. data/.ruby-version +1 -1
  7. data/.travis.yml +8 -4
  8. data/CHANGELOG.md +144 -1
  9. data/CODE_OF_CONDUCT.md +46 -0
  10. data/CONTRIBUTING.md +41 -0
  11. data/Gemfile +9 -4
  12. data/Guardfile +1 -0
  13. data/LICENSE +4 -199
  14. data/README.md +73 -344
  15. data/Rakefile +22 -18
  16. data/docs/HPKP.md +17 -0
  17. data/docs/cookies.md +64 -0
  18. data/docs/hashes.md +64 -0
  19. data/docs/named_overrides_and_appends.md +107 -0
  20. data/docs/per_action_configuration.md +133 -0
  21. data/docs/sinatra.md +25 -0
  22. data/lib/secure_headers/configuration.rb +116 -42
  23. data/lib/secure_headers/hash_helper.rb +2 -1
  24. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  25. data/lib/secure_headers/headers/content_security_policy.rb +128 -39
  26. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  27. data/lib/secure_headers/headers/cookie.rb +21 -3
  28. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  29. data/lib/secure_headers/headers/policy_management.rb +158 -68
  30. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  31. data/lib/secure_headers/headers/referrer_policy.rb +37 -0
  32. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  33. data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
  34. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  35. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  36. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  37. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  38. data/lib/secure_headers/middleware.rb +16 -9
  39. data/lib/secure_headers/railtie.rb +7 -6
  40. data/lib/secure_headers/utils/cookies_config.rb +13 -12
  41. data/lib/secure_headers/view_helper.rb +15 -8
  42. data/lib/secure_headers.rb +139 -55
  43. data/lib/tasks/tasks.rake +7 -5
  44. data/secure_headers.gemspec +13 -3
  45. data/spec/lib/secure_headers/configuration_spec.rb +13 -12
  46. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  47. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +89 -15
  48. data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
  49. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  50. data/spec/lib/secure_headers/headers/policy_management_spec.rb +80 -42
  51. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  52. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
  53. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  54. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  55. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  56. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  57. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  58. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  59. data/spec/lib/secure_headers/middleware_spec.rb +55 -17
  60. data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
  61. data/spec/lib/secure_headers_spec.rb +330 -58
  62. data/spec/spec_helper.rb +17 -25
  63. data/upgrading-to-3-0.md +13 -10
  64. data/upgrading-to-4-0.md +55 -0
  65. metadata +34 -7
@@ -1,33 +1,46 @@
1
- require_relative 'policy_management'
1
+ # frozen_string_literal: true
2
+ require_relative "policy_management"
3
+ require_relative "content_security_policy_config"
4
+ require "useragent"
2
5
 
3
6
  module SecureHeaders
4
- class ContentSecurityPolicyConfigError < StandardError; end
5
7
  class ContentSecurityPolicy
6
8
  include PolicyManagement
7
9
 
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
+
8
15
  def initialize(config = nil, user_agent = OTHER)
9
- config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
10
- @config = config
16
+ @config = if config.is_a?(Hash)
17
+ if config[:report_only]
18
+ ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
19
+ else
20
+ ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
21
+ end
22
+ elsif config.nil?
23
+ ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
24
+ else
25
+ config
26
+ end
27
+
11
28
  @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
12
29
  user_agent
13
30
  else
14
31
  UserAgent.parse(user_agent)
15
32
  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]
33
+ @frame_src = normalize_child_frame_src
34
+ @preserve_schemes = @config.preserve_schemes
35
+ @script_nonce = @config.script_nonce
36
+ @style_nonce = @config.style_nonce
20
37
  end
21
38
 
22
39
  ##
23
40
  # Returns the name to use for the header. Either "Content-Security-Policy" or
24
41
  # "Content-Security-Policy-Report-Only"
25
42
  def name
26
- if @report_only
27
- REPORT_ONLY
28
- else
29
- HEADER_NAME
30
- end
43
+ @config.class.const_get(:HEADER_NAME)
31
44
  end
32
45
 
33
46
  ##
@@ -42,6 +55,14 @@ module SecureHeaders
42
55
 
43
56
  private
44
57
 
58
+ def normalize_child_frame_src
59
+ if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
60
+ raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
61
+ end
62
+
63
+ @config.frame_src || @config.child_src
64
+ end
65
+
45
66
  # Private: converts the config object into a string representing a policy.
46
67
  # Places default-src at the first directive and report-uri as the last. All
47
68
  # others are presented in alphabetical order.
@@ -53,44 +74,90 @@ module SecureHeaders
53
74
  directives.map do |directive_name|
54
75
  case DIRECTIVE_VALUE_TYPES[directive_name]
55
76
  when :boolean
56
- symbol_to_hyphen_case(directive_name)
57
- when :string
58
- [symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
59
- else
60
- build_directive(directive_name)
77
+ symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
78
+ when :sandbox_list
79
+ build_sandbox_list_directive(directive_name)
80
+ when :media_type_list
81
+ build_media_type_list_directive(directive_name)
82
+ when :source_list
83
+ build_source_list_directive(directive_name)
61
84
  end
62
85
  end.compact.join("; ")
63
86
  end
64
87
 
88
+ def build_sandbox_list_directive(directive)
89
+ return unless sandbox_list = @config.directive_value(directive)
90
+ max_strict_policy = case sandbox_list
91
+ when Array
92
+ sandbox_list.empty?
93
+ when true
94
+ true
95
+ else
96
+ false
97
+ end
98
+
99
+ # A maximally strict sandbox policy is just the `sandbox` directive,
100
+ # whith no configuraiton values.
101
+ if max_strict_policy
102
+ symbol_to_hyphen_case(directive)
103
+ elsif sandbox_list && sandbox_list.any?
104
+ [
105
+ symbol_to_hyphen_case(directive),
106
+ sandbox_list.uniq
107
+ ].join(" ")
108
+ end
109
+ end
110
+
111
+ def build_media_type_list_directive(directive)
112
+ return unless media_type_list = @config.directive_value(directive)
113
+ if media_type_list && media_type_list.any?
114
+ [
115
+ symbol_to_hyphen_case(directive),
116
+ media_type_list.uniq
117
+ ].join(" ")
118
+ end
119
+ end
120
+
65
121
  # Private: builds a string that represents one directive in a minified form.
66
122
  #
67
123
  # directive_name - a symbol representing the various ALL_DIRECTIVES
68
124
  #
69
125
  # Returns a string representing a directive.
70
- def build_directive(directive)
71
- return if @config[directive].nil?
72
-
73
- source_list = @config[directive].compact
74
- return if source_list.empty?
126
+ def build_source_list_directive(directive)
127
+ source_list = case directive
128
+ when :child_src
129
+ if supported_directives.include?(:child_src)
130
+ @frame_src
131
+ end
132
+ when :frame_src
133
+ unless supported_directives.include?(:child_src)
134
+ @frame_src
135
+ end
136
+ else
137
+ @config.directive_value(directive)
138
+ end
75
139
 
76
- normalized_source_list = minify_source_list(directive, source_list)
77
- [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
140
+ if source_list != OPT_OUT && source_list && source_list.any?
141
+ normalized_source_list = minify_source_list(directive, source_list)
142
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
143
+ end
78
144
  end
79
145
 
80
146
  # If a directive contains *, all other values are omitted.
81
147
  # If a directive contains 'none' but has other values, 'none' is ommitted.
82
148
  # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
83
149
  def minify_source_list(directive, source_list)
150
+ source_list = source_list.compact
84
151
  if source_list.include?(STAR)
85
152
  keep_wildcard_sources(source_list)
86
153
  else
87
- populate_nonces!(directive, source_list)
88
- reject_all_values_if_none!(source_list)
154
+ source_list = populate_nonces(directive, source_list)
155
+ source_list = reject_all_values_if_none(source_list)
89
156
 
90
157
  unless directive == REPORT_URI || @preserve_schemes
91
- strip_source_schemes!(source_list)
158
+ source_list = strip_source_schemes(source_list)
92
159
  end
93
- dedup_source_list(source_list).join(" ")
160
+ dedup_source_list(source_list)
94
161
  end
95
162
  end
96
163
 
@@ -100,8 +167,12 @@ module SecureHeaders
100
167
  end
101
168
 
102
169
  # 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
170
+ def reject_all_values_if_none(source_list)
171
+ if source_list.length > 1
172
+ source_list.reject { |value| value == NONE }
173
+ else
174
+ source_list
175
+ end
105
176
  end
106
177
 
107
178
  # Removes duplicates and sources that already match an existing wild card.
@@ -123,12 +194,14 @@ module SecureHeaders
123
194
 
124
195
  # Private: append a nonce to the script/style directories if script_nonce
125
196
  # or style_nonce are provided.
126
- def populate_nonces!(directive, source_list)
197
+ def populate_nonces(directive, source_list)
127
198
  case directive
128
199
  when SCRIPT_SRC
129
200
  append_nonce(source_list, @script_nonce)
130
201
  when STYLE_SRC
131
202
  append_nonce(source_list, @style_nonce)
203
+ else
204
+ source_list
132
205
  end
133
206
  end
134
207
 
@@ -145,34 +218,50 @@ module SecureHeaders
145
218
  source_list << UNSAFE_INLINE
146
219
  end
147
220
  end
221
+
222
+ source_list
148
223
  end
149
224
 
150
225
  # Private: return the list of directives that are supported by the user agent,
151
226
  # starting with default-src and ending with report-uri.
152
227
  def directives
153
- [DEFAULT_SRC,
228
+ [
229
+ DEFAULT_SRC,
154
230
  BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
155
- REPORT_URI].flatten.select { |directive| @config.key?(directive) }
231
+ REPORT_URI
232
+ ].flatten
156
233
  end
157
234
 
158
235
  # 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, "") }
236
+ def strip_source_schemes(source_list)
237
+ source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
161
238
  end
162
239
 
163
240
  # Private: determine which directives are supported for the given user agent.
164
241
  #
242
+ # Add UA-sniffing special casing here.
243
+ #
165
244
  # Returns an array of symbols representing the directives.
166
245
  def supported_directives
167
- @supported_directives ||= VARIATIONS[@parsed_ua.browser] || VARIATIONS[OTHER]
246
+ @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
247
+ if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
248
+ VARIATIONS["FirefoxTransitional"]
249
+ elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
250
+ VARIATIONS["SafariTransitional"]
251
+ else
252
+ VARIATIONS[@parsed_ua.browser]
253
+ end
254
+ else
255
+ VARIATIONS[OTHER]
256
+ end
168
257
  end
169
258
 
170
259
  def nonces_supported?
171
- @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser)
260
+ @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
172
261
  end
173
262
 
174
263
  def symbol_to_hyphen_case(sym)
175
- sym.to_s.tr('_', '-')
264
+ sym.to_s.tr("_", "-")
176
265
  end
177
266
  end
178
267
  end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ module DynamicConfig
4
+ def self.included(base)
5
+ base.send(:attr_writer, :modified)
6
+ base.send(:attr_reader, *base.attrs)
7
+ base.attrs.each do |attr|
8
+ base.send(:define_method, "#{attr}=") do |value|
9
+ if self.class.attrs.include?(attr)
10
+ write_attribute(attr, value)
11
+ else
12
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(hash)
19
+ @base_uri = nil
20
+ @block_all_mixed_content = nil
21
+ @child_src = nil
22
+ @connect_src = nil
23
+ @default_src = nil
24
+ @font_src = nil
25
+ @form_action = nil
26
+ @frame_ancestors = nil
27
+ @frame_src = nil
28
+ @img_src = nil
29
+ @manifest_src = nil
30
+ @media_src = nil
31
+ @object_src = nil
32
+ @plugin_types = nil
33
+ @preserve_schemes = nil
34
+ @report_only = nil
35
+ @report_uri = nil
36
+ @sandbox = nil
37
+ @script_nonce = nil
38
+ @script_src = nil
39
+ @style_nonce = nil
40
+ @style_src = nil
41
+ @upgrade_insecure_requests = nil
42
+
43
+ from_hash(hash)
44
+ @modified = false
45
+ end
46
+
47
+ def update_directive(directive, value)
48
+ self.send("#{directive}=", value)
49
+ end
50
+
51
+ def directive_value(directive)
52
+ if self.class.attrs.include?(directive)
53
+ self.send(directive)
54
+ end
55
+ end
56
+
57
+ def modified?
58
+ @modified
59
+ end
60
+
61
+ def merge(new_hash)
62
+ ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
63
+ end
64
+
65
+ def merge!(new_hash)
66
+ from_hash(new_hash)
67
+ end
68
+
69
+ def append(new_hash)
70
+ from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash))
71
+ end
72
+
73
+ def to_h
74
+ self.class.attrs.each_with_object({}) do |key, hash|
75
+ value = self.send(key)
76
+ hash[key] = value unless value.nil?
77
+ end
78
+ end
79
+
80
+ def dup
81
+ self.class.new(self.to_h)
82
+ end
83
+
84
+ def opt_out?
85
+ false
86
+ end
87
+
88
+ def ==(o)
89
+ self.class == o.class && self.to_h == o.to_h
90
+ end
91
+
92
+ alias_method :[], :directive_value
93
+ alias_method :[]=, :update_directive
94
+
95
+ private
96
+ def from_hash(hash)
97
+ hash.each_pair do |k, v|
98
+ next if v.nil?
99
+
100
+ if self.class.attrs.include?(k)
101
+ write_attribute(k, v)
102
+ else
103
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
104
+ end
105
+ end
106
+ end
107
+
108
+ def write_attribute(attr, value)
109
+ value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
110
+ attr_variable = "@#{attr}"
111
+ prev_value = self.instance_variable_get(attr_variable)
112
+ self.instance_variable_set(attr_variable, value)
113
+ if prev_value != value
114
+ @modified = true
115
+ end
116
+ end
117
+ end
118
+
119
+ class ContentSecurityPolicyConfigError < StandardError; end
120
+ class ContentSecurityPolicyConfig
121
+ CONFIG_KEY = :csp
122
+ HEADER_NAME = "Content-Security-Policy".freeze
123
+
124
+ ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
125
+ def self.attrs
126
+ ATTRS
127
+ end
128
+
129
+ include DynamicConfig
130
+
131
+ # based on what was suggested in https://github.com/rails/rails/pull/24961/files
132
+ DEFAULT = {
133
+ default_src: %w('self' https:),
134
+ font_src: %w('self' https: data:),
135
+ img_src: %w('self' https: data:),
136
+ object_src: %w('none'),
137
+ script_src: %w(https:),
138
+ style_src: %w('self' https: 'unsafe-inline')
139
+ }
140
+
141
+ def report_only?
142
+ false
143
+ end
144
+
145
+ def make_report_only
146
+ ContentSecurityPolicyReportOnlyConfig.new(self.to_h)
147
+ end
148
+ end
149
+
150
+ class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
151
+ CONFIG_KEY = :csp_report_only
152
+ HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
153
+
154
+ def report_only?
155
+ true
156
+ end
157
+
158
+ def make_report_only
159
+ self
160
+ end
161
+ end
162
+ end
@@ -1,5 +1,7 @@
1
- require 'cgi'
2
- require 'secure_headers/utils/cookies_config'
1
+ # frozen_string_literal: true
2
+ require "cgi"
3
+ require "secure_headers/utils/cookies_config"
4
+
3
5
 
4
6
  module SecureHeaders
5
7
  class CookiesConfigError < StandardError; end
@@ -13,8 +15,18 @@ module SecureHeaders
13
15
 
14
16
  attr_reader :raw_cookie, :config
15
17
 
18
+ COOKIE_DEFAULTS = {
19
+ httponly: true,
20
+ secure: true,
21
+ samesite: { lax: true },
22
+ }.freeze
23
+
16
24
  def initialize(cookie, config)
17
25
  @raw_cookie = cookie
26
+ unless config == OPT_OUT
27
+ config ||= {}
28
+ config = COOKIE_DEFAULTS.merge(config)
29
+ end
18
30
  @config = config
19
31
  @attributes = {
20
32
  httponly: nil,
@@ -56,6 +68,7 @@ module SecureHeaders
56
68
  end
57
69
 
58
70
  def flag_cookie?(attribute)
71
+ return false if config == OPT_OUT
59
72
  case config[attribute]
60
73
  when TrueClass
61
74
  true
@@ -85,6 +98,7 @@ module SecureHeaders
85
98
  end
86
99
 
87
100
  def flag_samesite?
101
+ return false if config == OPT_OUT || config[:samesite] == OPT_OUT
88
102
  flag_samesite_lax? || flag_samesite_strict?
89
103
  end
90
104
 
@@ -99,6 +113,10 @@ module SecureHeaders
99
113
  def flag_samesite_enforcement?(mode)
100
114
  return unless config[:samesite]
101
115
 
116
+ if config[:samesite].is_a?(TrueClass) && mode == :lax
117
+ return true
118
+ end
119
+
102
120
  case config[:samesite][mode]
103
121
  when Hash
104
122
  conditionally_flag?(config[:samesite][mode])
@@ -113,7 +131,7 @@ module SecureHeaders
113
131
  return unless cookie
114
132
 
115
133
  cookie.split(/[;,]\s?/).each do |pairs|
116
- name, values = pairs.split('=',2)
134
+ name, values = pairs.split("=", 2)
117
135
  name = CGI.unescape(name)
118
136
 
119
137
  attribute = name.downcase.to_sym
@@ -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
+ [
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