secure_headers 3.5.0 → 3.6.1

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.
@@ -0,0 +1,107 @@
1
+ ## Named Appends
2
+
3
+ Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility.
4
+
5
+ ```ruby
6
+ def show
7
+ if include_widget?
8
+ @widget = widget.render
9
+ use_content_security_policy_named_append(:widget_partial)
10
+ end
11
+ end
12
+
13
+
14
+ SecureHeaders::Configuration.named_append(:widget_partial) do |request|
15
+ SecureHeaders.override_x_frame_options(request, "DENY")
16
+ if request.controller_instance.current_user.in_test_bucket?
17
+ { child_src: %w(beta.thirdpartyhost.com) }
18
+ else
19
+ { child_src: %w(thirdpartyhost.com) }
20
+ end
21
+ end
22
+ ```
23
+
24
+ You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
25
+
26
+ ```ruby
27
+ SecureHeader::Configuration.default do |config|
28
+ config.csp = { default_src: %w('self')}
29
+ end
30
+
31
+ SecureHeaders::Configuration.named_append(:A) do |request|
32
+ { default_src: %w(myhost.com) }
33
+ end
34
+
35
+ SecureHeaders::Configuration.named_append(:B) do |request|
36
+ { script_src: %w('unsafe-eval') }
37
+ end
38
+ ```
39
+
40
+ The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.):
41
+
42
+ ```ruby
43
+ def index
44
+ use_content_security_policy_named_append(:A)
45
+ use_content_security_policy_named_append(:B)
46
+ # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
47
+ end
48
+
49
+ def show
50
+ use_content_security_policy_named_append(:B)
51
+ use_content_security_policy_named_append(:A)
52
+ # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
53
+ end
54
+ ```
55
+
56
+
57
+ ## Named overrides
58
+
59
+ Named overrides serve two purposes:
60
+
61
+ * To be able to refer to a configuration by simple name.
62
+ * By precomputing the headers for a named configuration, the headers generated once and reused over every request.
63
+
64
+ To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override.
65
+
66
+ ```ruby
67
+ class ApplicationController < ActionController::Base
68
+ SecureHeaders::Configuration.default do |config|
69
+ config.csp = {
70
+ default_src: %w('self'),
71
+ script_src: %w(example.org)
72
+ }
73
+ end
74
+
75
+ # override default configuration
76
+ SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config|
77
+ config.csp[:script_src] << "otherdomain.com"
78
+ end
79
+
80
+ # overrides the :script_from_otherdomain_com configuration
81
+ SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config|
82
+ config.csp[:script_src] << "evenanotherdomain.com"
83
+ end
84
+ end
85
+
86
+ class MyController < ApplicationController
87
+ def index
88
+ # Produces default-src 'self'; script-src example.org otherdomain.com
89
+ use_secure_headers_override(:script_from_otherdomain_com)
90
+ end
91
+
92
+ def show
93
+ # Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com
94
+ use_secure_headers_override(:another_config)
95
+ end
96
+ end
97
+ ```
98
+
99
+ By default, a no-op configuration is provided. No headers will be set when this default override is used.
100
+
101
+ ```ruby
102
+ class MyController < ApplicationController
103
+ def index
104
+ SecureHeaders.opt_out_of_all_protection(request)
105
+ end
106
+ end
107
+ ```
@@ -0,0 +1,105 @@
1
+ ## Per-action configuration
2
+
3
+ You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request.
4
+
5
+ ```ruby
6
+ # Given a config of:
7
+ ::SecureHeaders::Configuration.default do |config|
8
+ config.csp = {
9
+ default_src: %w('self'),
10
+ script_src: %w('self')
11
+ }
12
+ end
13
+
14
+ class MyController < ApplicationController
15
+ def index
16
+ # Append value to the source list, override 'none' values
17
+ # Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com
18
+ append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com))
19
+
20
+ # Overrides the previously set source list, override 'none' values
21
+ # Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self'
22
+ override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self'))
23
+
24
+ # Global settings default to "sameorigin"
25
+ override_x_frame_options("DENY")
26
+ end
27
+ ```
28
+
29
+ The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object.
30
+ * `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration.
31
+ * `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config
32
+ * `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value`
33
+
34
+ ## Appending / overriding Content Security Policy
35
+
36
+ When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`.
37
+
38
+ #### Append to the policy with a directive other than `default_src`
39
+
40
+ The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate:
41
+
42
+ ```ruby
43
+ ::SecureHeaders::Configuration.default do |config|
44
+ config.csp = {
45
+ default_src: %w('self')
46
+ }
47
+ end
48
+ ```
49
+
50
+ Code | Result
51
+ ------------- | -------------
52
+ `append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com`
53
+ `override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com`
54
+
55
+ #### Nonce
56
+
57
+ You can use a view helper to automatically add nonces to script tags:
58
+
59
+ ```erb
60
+ <%= nonced_javascript_tag do %>
61
+ console.log("nonced!");
62
+ <% end %>
63
+
64
+ <%= nonced_style_tag do %>
65
+ body {
66
+ background-color: black;
67
+ }
68
+ <% end %>
69
+ ```
70
+
71
+ becomes:
72
+
73
+ ```html
74
+ <script nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
75
+ console.log("nonced!")
76
+ </script>
77
+ <style nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
78
+ body {
79
+ background-color: black;
80
+ }
81
+ </style>
82
+ ```
83
+
84
+ ```
85
+
86
+ Content-Security-Policy: ...
87
+ script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
88
+ style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
89
+ ```
90
+
91
+ `script`/`style-nonce` can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags.
92
+
93
+ ```erb
94
+ <script nonce="<%= content_security_policy_script_nonce %>">
95
+ console.log("whitelisted, will execute")
96
+ </script>
97
+
98
+ <script nonce="lol">
99
+ console.log("won't execute, not whitelisted")
100
+ </script>
101
+
102
+ <script>
103
+ console.log("won't execute, not whitelisted")
104
+ </script>
105
+ ```
data/docs/sinatra.md ADDED
@@ -0,0 +1,25 @@
1
+ ## Sinatra
2
+
3
+ Here's an example using SecureHeaders for Sinatra applications:
4
+
5
+ ```ruby
6
+ require 'rubygems'
7
+ require 'sinatra'
8
+ require 'haml'
9
+ require 'secure_headers'
10
+
11
+ use SecureHeaders::Middleware
12
+
13
+ SecureHeaders::Configuration.default do |config|
14
+ ...
15
+ end
16
+
17
+ class Donkey < Sinatra::Application
18
+ set :root, APP_ROOT
19
+
20
+ get '/' do
21
+ SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT)
22
+ haml :index
23
+ end
24
+ end
25
+ ```
@@ -116,7 +116,7 @@ module SecureHeaders
116
116
 
117
117
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
118
118
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
119
- :referrer_policy
119
+ :referrer_policy, :clear_site_data
120
120
 
121
121
  attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
122
122
 
@@ -150,6 +150,7 @@ module SecureHeaders
150
150
  copy.x_xss_protection = @x_xss_protection
151
151
  copy.x_download_options = @x_download_options
152
152
  copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
153
+ copy.clear_site_data = @clear_site_data
153
154
  copy.referrer_policy = @referrer_policy
154
155
  copy.hpkp = @hpkp
155
156
  copy.hpkp_report_host = @hpkp_report_host
@@ -181,6 +182,7 @@ module SecureHeaders
181
182
  XXssProtection.validate_config!(@x_xss_protection)
182
183
  XDownloadOptions.validate_config!(@x_download_options)
183
184
  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
185
+ ClearSiteData.validate_config!(@clear_site_data)
184
186
  PublicKeyPins.validate_config!(@hpkp)
185
187
  Cookie.validate_config!(@cookies)
186
188
  end
@@ -260,7 +262,7 @@ module SecureHeaders
260
262
  end
261
263
  end
262
264
 
263
- # Public: Precompute the header names and values for this configuraiton.
265
+ # Public: Precompute the header names and values for this configuration.
264
266
  # Ensures that headers generated at configure time, not on demand.
265
267
  #
266
268
  # Returns the cached headers
@@ -0,0 +1,51 @@
1
+ module SecureHeaders
2
+ class ClearSiteDataConfigError < StandardError; end
3
+ class ClearSiteData
4
+ HEADER_NAME = "Clear-Site-Data".freeze
5
+ TYPES = "types".freeze
6
+
7
+ # Valid `types`
8
+ CACHE = "cache".freeze
9
+ COOKIES = "cookies".freeze
10
+ STORAGE = "storage".freeze
11
+ EXECTION_CONTEXTS = "executionContexts".freeze
12
+ ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_CONTEXTS]
13
+
14
+ CONFIG_KEY = :clear_site_data
15
+
16
+ class << self
17
+ # Public: make an Clear-Site-Data header name, value pair
18
+ #
19
+ # Returns nil if not configured, returns header name and value if configured.
20
+ def make_header(config=nil)
21
+ case config
22
+ when nil, OPT_OUT, []
23
+ # noop
24
+ when Array
25
+ [HEADER_NAME, JSON.dump(TYPES => config)]
26
+ when true
27
+ [HEADER_NAME, JSON.dump(TYPES => ALL_TYPES)]
28
+ end
29
+ end
30
+
31
+ def validate_config!(config)
32
+ case config
33
+ when nil, OPT_OUT, true
34
+ # valid
35
+ when Array
36
+ unless config.all? { |t| t.is_a?(String) }
37
+ raise ClearSiteDataConfigError.new("types must be Strings")
38
+ end
39
+
40
+ begin
41
+ JSON.dump(config)
42
+ rescue JSON::GeneratorError, Encoding::UndefinedConversionError
43
+ raise ClearSiteDataConfigError.new("types must serializable by JSON")
44
+ end
45
+ else
46
+ raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -9,6 +9,7 @@ module SecureHeaders
9
9
  # constants to be used for version-specific UA sniffing
10
10
  VERSION_46 = ::UserAgent::Version.new("46")
11
11
  VERSION_10 = ::UserAgent::Version.new("10")
12
+ FALLBACK_VERSION = ::UserAgent::Version.new("0")
12
13
 
13
14
  def initialize(config = nil, user_agent = OTHER)
14
15
  @config = if config.is_a?(Hash)
@@ -213,7 +214,7 @@ module SecureHeaders
213
214
  # Returns an array of symbols representing the directives.
214
215
  def supported_directives
215
216
  @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
216
- if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
217
+ if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
217
218
  VARIATIONS["FirefoxTransitional"]
218
219
  else
219
220
  VARIATIONS[@parsed_ua.browser]
@@ -6,12 +6,7 @@ module SecureHeaders
6
6
  base.attrs.each do |attr|
7
7
  base.send(:define_method, "#{attr}=") do |value|
8
8
  if self.class.attrs.include?(attr)
9
- value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
10
- prev_value = self.instance_variable_get("@#{attr}")
11
- self.instance_variable_set("@#{attr}", value)
12
- if prev_value != self.instance_variable_get("@#{attr}")
13
- @modified = true
14
- end
9
+ write_attribute(attr, value)
15
10
  else
16
11
  raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
17
12
  end
@@ -52,8 +47,9 @@ module SecureHeaders
52
47
 
53
48
  def to_h
54
49
  self.class.attrs.each_with_object({}) do |key, hash|
55
- hash[key] = self.send(key)
56
- end.reject { |_, v| v.nil? }
50
+ value = self.send(key)
51
+ hash[key] = value unless value.nil?
52
+ end
57
53
  end
58
54
 
59
55
  def dup
@@ -73,14 +69,26 @@ module SecureHeaders
73
69
 
74
70
  private
75
71
  def from_hash(hash)
76
- hash.keys.reject { |k| hash[k].nil? }.map do |k|
72
+ hash.each_pair do |k, v|
73
+ next if v.nil?
74
+
77
75
  if self.class.attrs.include?(k)
78
- self.send("#{k}=", hash[k])
76
+ write_attribute(k, v)
79
77
  else
80
- raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}"
78
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
81
79
  end
82
80
  end
83
81
  end
82
+
83
+ def write_attribute(attr, value)
84
+ value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
85
+ attr_variable = "@#{attr}"
86
+ prev_value = self.instance_variable_get(attr_variable)
87
+ self.instance_variable_set(attr_variable, value)
88
+ if prev_value != value
89
+ @modified = true
90
+ end
91
+ end
84
92
  end
85
93
 
86
94
  class ContentSecurityPolicyConfigError < StandardError; end
@@ -88,8 +96,9 @@ module SecureHeaders
88
96
  CONFIG_KEY = :csp
89
97
  HEADER_NAME = "Content-Security-Policy".freeze
90
98
 
99
+ ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
91
100
  def self.attrs
92
- PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
101
+ ATTRS
93
102
  end
94
103
 
95
104
  include DynamicConfig
@@ -14,6 +14,7 @@ module SecureHeaders
14
14
  STAR = "*".freeze
15
15
  UNSAFE_INLINE = "'unsafe-inline'".freeze
16
16
  UNSAFE_EVAL = "'unsafe-eval'".freeze
17
+ STRICT_DYNAMIC = "'strict-dynamic'".freeze
17
18
 
18
19
  # leftover deprecated values that will be in common use upon upgrading.
19
20
  DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
@@ -108,7 +109,7 @@ module SecureHeaders
108
109
  DIRECTIVES_2_0 + DIRECTIVES_DRAFT
109
110
  ).freeze
110
111
 
111
- ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
112
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT).uniq.sort
112
113
 
113
114
  # Think of default-src and report-uri as the beginning and end respectively,
114
115
  # everything else is in between.
@@ -217,7 +218,7 @@ module SecureHeaders
217
218
  def nonces_supported?(user_agent)
218
219
  user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
219
220
  MODERN_BROWSERS.include?(user_agent.browser) ||
220
- user_agent.browser == "Safari" && user_agent.version >= CSP::VERSION_10
221
+ user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
221
222
  end
222
223
 
223
224
  # Public: combine the values from two different configs.
@@ -270,7 +271,7 @@ module SecureHeaders
270
271
  # copy the default-src value to the original config. This modifies the original hash.
271
272
  def populate_fetch_source_with_default!(original, additions)
272
273
  # in case we would be appending to an empty directive, fill it with the default-src value
273
- additions.keys.each do |directive|
274
+ additions.each_key do |directive|
274
275
  if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
275
276
  if nonce_added?(original, additions)
276
277
  inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
@@ -1,11 +1,14 @@
1
1
  module SecureHeaders
2
2
  class ReferrerPolicyConfigError < StandardError; end
3
3
  class ReferrerPolicy
4
- HEADER_NAME = "Referrer-Policy"
4
+ HEADER_NAME = "Referrer-Policy".freeze
5
5
  DEFAULT_VALUE = "origin-when-cross-origin"
6
6
  VALID_POLICIES = %w(
7
7
  no-referrer
8
8
  no-referrer-when-downgrade
9
+ same-origin
10
+ strict-origin
11
+ strict-origin-when-cross-origin
9
12
  origin
10
13
  origin-when-cross-origin
11
14
  unsafe-url
@@ -2,7 +2,7 @@ module SecureHeaders
2
2
  class STSConfigError < StandardError; end
3
3
 
4
4
  class StrictTransportSecurity
5
- HEADER_NAME = 'Strict-Transport-Security'
5
+ HEADER_NAME = 'Strict-Transport-Security'.freeze
6
6
  HSTS_MAX_AGE = "631138519"
7
7
  DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
8
8
  VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
@@ -2,7 +2,7 @@ module SecureHeaders
2
2
  class XContentTypeOptionsConfigError < StandardError; end
3
3
 
4
4
  class XContentTypeOptions
5
- HEADER_NAME = "X-Content-Type-Options"
5
+ HEADER_NAME = "X-Content-Type-Options".freeze
6
6
  DEFAULT_VALUE = "nosniff"
7
7
  CONFIG_KEY = :x_content_type_options
8
8
 
@@ -1,7 +1,7 @@
1
1
  module SecureHeaders
2
2
  class XDOConfigError < StandardError; end
3
3
  class XDownloadOptions
4
- HEADER_NAME = "X-Download-Options"
4
+ HEADER_NAME = "X-Download-Options".freeze
5
5
  DEFAULT_VALUE = 'noopen'
6
6
  CONFIG_KEY = :x_download_options
7
7
 
@@ -1,7 +1,7 @@
1
1
  module SecureHeaders
2
2
  class XFOConfigError < StandardError; end
3
3
  class XFrameOptions
4
- HEADER_NAME = "X-Frame-Options"
4
+ HEADER_NAME = "X-Frame-Options".freeze
5
5
  CONFIG_KEY = :x_frame_options
6
6
  SAMEORIGIN = "sameorigin"
7
7
  DENY = "deny"
@@ -1,7 +1,7 @@
1
1
  module SecureHeaders
2
2
  class XPCDPConfigError < StandardError; end
3
3
  class XPermittedCrossDomainPolicies
4
- HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
4
+ HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
5
5
  DEFAULT_VALUE = 'none'
6
6
  VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
7
7
  CONFIG_KEY = :x_permitted_cross_domain_policies
@@ -1,7 +1,7 @@
1
1
  module SecureHeaders
2
2
  class XXssProtectionConfigError < StandardError; end
3
3
  class XXssProtection
4
- HEADER_NAME = 'X-XSS-Protection'
4
+ HEADER_NAME = 'X-XSS-Protection'.freeze
5
5
  DEFAULT_VALUE = "1; mode=block"
6
6
  VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
7
7
  CONFIG_KEY = :x_xss_protection
@@ -10,6 +10,7 @@ require "secure_headers/headers/x_content_type_options"
10
10
  require "secure_headers/headers/x_download_options"
11
11
  require "secure_headers/headers/x_permitted_cross_domain_policies"
12
12
  require "secure_headers/headers/referrer_policy"
13
+ require "secure_headers/headers/clear_site_data"
13
14
  require "secure_headers/middleware"
14
15
  require "secure_headers/railtie"
15
16
  require "secure_headers/view_helper"
@@ -50,6 +51,7 @@ module SecureHeaders
50
51
  CSP = ContentSecurityPolicy
51
52
 
52
53
  ALL_HEADER_CLASSES = [
54
+ ClearSiteData,
53
55
  ContentSecurityPolicyConfig,
54
56
  ContentSecurityPolicyReportOnlyConfig,
55
57
  StrictTransportSecurity,
@@ -175,7 +177,7 @@ module SecureHeaders
175
177
 
176
178
  header_classes_for(request).each_with_object({}) do |klass, hash|
177
179
  if header = headers[klass::CONFIG_KEY]
178
- header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass)
180
+ header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
179
181
  csp_header_for_ua(header, user_agent)
180
182
  else
181
183
  header
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |gem|
3
3
  gem.name = "secure_headers"
4
- gem.version = "3.5.0"
4
+ gem.version = "3.6.1"
5
5
  gem.authors = ["Neil Matatall"]
6
6
  gem.email = ["neil.matatall@gmail.com"]
7
7
  gem.description = 'Security related headers all in one gem.'
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ module SecureHeaders
4
+ describe ClearSiteData do
5
+ describe "make_header" do
6
+ it "returns nil with nil config" do
7
+ expect(described_class.make_header).to be_nil
8
+ end
9
+
10
+ it "returns nil with empty config" do
11
+ expect(described_class.make_header([])).to be_nil
12
+ end
13
+
14
+ it "returns nil with opt-out config" do
15
+ expect(described_class.make_header(OPT_OUT)).to be_nil
16
+ end
17
+
18
+ it "returns all types with `true` config" do
19
+ name, value = described_class.make_header(true)
20
+
21
+ expect(name).to eq(ClearSiteData::HEADER_NAME)
22
+ expect(value).to eq(normalize_json(<<-HERE))
23
+ {
24
+ "types": [
25
+ "cache",
26
+ "cookies",
27
+ "storage",
28
+ "executionContexts"
29
+ ]
30
+ }
31
+ HERE
32
+ end
33
+
34
+ it "returns specified types" do
35
+ name, value = described_class.make_header(["foo", "bar"])
36
+
37
+ expect(name).to eq(ClearSiteData::HEADER_NAME)
38
+ expect(value).to eq(normalize_json(<<-HERE))
39
+ {
40
+ "types": [
41
+ "foo",
42
+ "bar"
43
+ ]
44
+ }
45
+ HERE
46
+ end
47
+ end
48
+
49
+ describe "validate_config!" do
50
+ it "succeeds for `true` config" do
51
+ expect do
52
+ described_class.validate_config!(true)
53
+ end.not_to raise_error
54
+ end
55
+
56
+ it "succeeds for `nil` config" do
57
+ expect do
58
+ described_class.validate_config!(nil)
59
+ end.not_to raise_error
60
+ end
61
+
62
+ it "succeeds for opt-out config" do
63
+ expect do
64
+ described_class.validate_config!(OPT_OUT)
65
+ end.not_to raise_error
66
+ end
67
+
68
+ it "succeeds for empty config" do
69
+ expect do
70
+ described_class.validate_config!([])
71
+ end.not_to raise_error
72
+ end
73
+
74
+ it "succeeds for Array of Strings config" do
75
+ expect do
76
+ described_class.validate_config!(["foo"])
77
+ end.not_to raise_error
78
+ end
79
+
80
+ it "fails for Array of non-String config" do
81
+ expect do
82
+ described_class.validate_config!([1])
83
+ end.to raise_error(ClearSiteDataConfigError)
84
+ end
85
+
86
+ it "fails for non-serializable config" do
87
+ expect do
88
+ described_class.validate_config!(["hi \255"])
89
+ end.to raise_error(ClearSiteDataConfigError)
90
+ end
91
+
92
+ it "fails for other types of config" do
93
+ expect do
94
+ described_class.validate_config!(:cookies)
95
+ end.to raise_error(ClearSiteDataConfigError)
96
+ end
97
+ end
98
+
99
+ def normalize_json(json)
100
+ JSON.dump(JSON.parse(json))
101
+ end
102
+ end
103
+ end