secure_headers 3.6.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +2 -0
  5. data/CHANGELOG.md +34 -1
  6. data/CODE_OF_CONDUCT.md +46 -0
  7. data/CONTRIBUTING.md +41 -0
  8. data/LICENSE +4 -199
  9. data/README.md +12 -3
  10. data/docs/hashes.md +1 -1
  11. data/lib/secure_headers/configuration.rb +25 -5
  12. data/lib/secure_headers/headers/clear_site_data.rb +12 -9
  13. data/lib/secure_headers/headers/content_security_policy.rb +2 -2
  14. data/lib/secure_headers/headers/content_security_policy_config.rb +45 -12
  15. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  16. data/lib/secure_headers/headers/policy_management.rb +22 -22
  17. data/lib/secure_headers/headers/public_key_pins.rb +1 -1
  18. data/lib/secure_headers/headers/referrer_policy.rb +1 -1
  19. data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
  20. data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
  21. data/lib/secure_headers/headers/x_download_options.rb +1 -1
  22. data/lib/secure_headers/headers/x_frame_options.rb +1 -1
  23. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
  24. data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
  25. data/lib/secure_headers/view_helper.rb +2 -2
  26. data/lib/secure_headers.rb +5 -2
  27. data/secure_headers.gemspec +2 -2
  28. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +9 -26
  29. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +12 -8
  30. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  31. data/spec/lib/secure_headers/headers/policy_management_spec.rb +1 -0
  32. data/spec/lib/secure_headers/view_helpers_spec.rb +0 -1
  33. data/spec/lib/secure_headers_spec.rb +31 -4
  34. data/spec/spec_helper.rb +1 -0
  35. metadata +9 -4
@@ -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
@@ -20,6 +15,30 @@ module SecureHeaders
20
15
  end
21
16
 
22
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
+
23
42
  from_hash(hash)
24
43
  @modified = false
25
44
  end
@@ -52,8 +71,9 @@ module SecureHeaders
52
71
 
53
72
  def to_h
54
73
  self.class.attrs.each_with_object({}) do |key, hash|
55
- hash[key] = self.send(key)
56
- end.reject { |_, v| v.nil? }
74
+ value = self.send(key)
75
+ hash[key] = value unless value.nil?
76
+ end
57
77
  end
58
78
 
59
79
  def dup
@@ -73,14 +93,26 @@ module SecureHeaders
73
93
 
74
94
  private
75
95
  def from_hash(hash)
76
- hash.keys.reject { |k| hash[k].nil? }.map do |k|
96
+ hash.each_pair do |k, v|
97
+ next if v.nil?
98
+
77
99
  if self.class.attrs.include?(k)
78
- self.send("#{k}=", hash[k])
100
+ write_attribute(k, v)
79
101
  else
80
- raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}"
102
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
81
103
  end
82
104
  end
83
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
84
116
  end
85
117
 
86
118
  class ContentSecurityPolicyConfigError < StandardError; end
@@ -88,8 +120,9 @@ module SecureHeaders
88
120
  CONFIG_KEY = :csp
89
121
  HEADER_NAME = "Content-Security-Policy".freeze
90
122
 
123
+ ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
91
124
  def self.attrs
92
- PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
125
+ ATTRS
93
126
  end
94
127
 
95
128
  include DynamicConfig
@@ -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
@@ -62,25 +62,19 @@ module SecureHeaders
62
62
 
63
63
  # All the directives currently under consideration for CSP level 3.
64
64
  # https://w3c.github.io/webappsec/specs/CSP2/
65
+ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
65
66
  MANIFEST_SRC = :manifest_src
66
- REFLECTED_XSS = :reflected_xss
67
+ UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
67
68
  DIRECTIVES_3_0 = [
68
69
  DIRECTIVES_2_0,
69
- MANIFEST_SRC,
70
- REFLECTED_XSS
71
- ].flatten.freeze
72
-
73
- # All the directives that are not currently in a formal spec, but have
74
- # been implemented somewhere.
75
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
76
- UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
77
- DIRECTIVES_DRAFT = [
78
70
  BLOCK_ALL_MIXED_CONTENT,
71
+ MANIFEST_SRC,
79
72
  UPGRADE_INSECURE_REQUESTS
80
- ].freeze
73
+ ].flatten.freeze
81
74
 
82
75
  EDGE_DIRECTIVES = DIRECTIVES_1_0
83
76
  SAFARI_DIRECTIVES = DIRECTIVES_1_0
77
+ SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
84
78
 
85
79
  FIREFOX_UNSUPPORTED_DIRECTIVES = [
86
80
  BLOCK_ALL_MIXED_CONTENT,
@@ -98,18 +92,18 @@ module SecureHeaders
98
92
  ].freeze
99
93
 
100
94
  FIREFOX_DIRECTIVES = (
101
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
95
+ DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
102
96
  ).freeze
103
97
 
104
98
  FIREFOX_46_DIRECTIVES = (
105
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
99
+ DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
106
100
  ).freeze
107
101
 
108
102
  CHROME_DIRECTIVES = (
109
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
103
+ DIRECTIVES_3_0
110
104
  ).freeze
111
105
 
112
- 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
113
107
 
114
108
  # Think of default-src and report-uri as the beginning and end respectively,
115
109
  # everything else is in between.
@@ -133,6 +127,7 @@ module SecureHeaders
133
127
  "Firefox" => FIREFOX_DIRECTIVES,
134
128
  "FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
135
129
  "Safari" => SAFARI_DIRECTIVES,
130
+ "SafariTransitional" => SAFARI_10_DIRECTIVES,
136
131
  "Edge" => EDGE_DIRECTIVES,
137
132
  "Other" => CHROME_DIRECTIVES
138
133
  }.freeze
@@ -154,7 +149,6 @@ module SecureHeaders
154
149
  MEDIA_SRC => :source_list,
155
150
  OBJECT_SRC => :source_list,
156
151
  PLUGIN_TYPES => :source_list,
157
- REFLECTED_XSS => :string,
158
152
  REPORT_URI => :source_list,
159
153
  SANDBOX => :source_list,
160
154
  SCRIPT_SRC => :source_list,
@@ -271,20 +265,26 @@ module SecureHeaders
271
265
  # copy the default-src value to the original config. This modifies the original hash.
272
266
  def populate_fetch_source_with_default!(original, additions)
273
267
  # in case we would be appending to an empty directive, fill it with the default-src value
274
- additions.keys.each do |directive|
268
+ additions.each_key do |directive|
275
269
  if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
276
270
  if nonce_added?(original, additions)
277
271
  inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
278
272
  unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
279
- original[inferred_directive] = original[:default_src]
273
+ original[inferred_directive] = default_for(directive, original)
280
274
  end
281
275
  else
282
- original[directive] = original[:default_src]
276
+ original[directive] = default_for(directive, original)
283
277
  end
284
278
  end
285
279
  end
286
280
  end
287
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
288
  def nonce_added?(original, additions)
289
289
  [:script_nonce, :style_nonce].each do |nonce|
290
290
  if additions[nonce] && !original[nonce]
@@ -340,9 +340,9 @@ module SecureHeaders
340
340
  end
341
341
 
342
342
  def ensure_valid_sources!(directive, source_expression)
343
- source_expression.each do |source_expression|
344
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
345
- 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.")
346
346
  end
347
347
  end
348
348
  end
@@ -49,7 +49,7 @@ module SecureHeaders
49
49
  end
50
50
 
51
51
  def value
52
- header_value = [
52
+ [
53
53
  max_age_directive,
54
54
  pin_directives,
55
55
  report_uri_directive,
@@ -1,7 +1,7 @@
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
@@ -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
@@ -95,9 +95,9 @@ module SecureHeaders
95
95
  <<-EOF
96
96
  \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
97
97
  #{content}
98
- *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
98
+ *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
99
99
  #{file_path}:
100
- - #{hash_value}\n\n
100
+ - \"#{hash_value}\"\n\n
101
101
  NOTE: dynamic javascript is not supported using script hash integration
102
102
  on purpose. It defeats the point of using it in the first place.
103
103
  EOF
@@ -11,6 +11,7 @@ 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
13
  require "secure_headers/headers/clear_site_data"
14
+ require "secure_headers/headers/expect_certificate_transparency"
14
15
  require "secure_headers/middleware"
15
16
  require "secure_headers/railtie"
16
17
  require "secure_headers/view_helper"
@@ -51,6 +52,7 @@ module SecureHeaders
51
52
  CSP = ContentSecurityPolicy
52
53
 
53
54
  ALL_HEADER_CLASSES = [
55
+ ExpectCertificateTransparency,
54
56
  ClearSiteData,
55
57
  ContentSecurityPolicyConfig,
56
58
  ContentSecurityPolicyReportOnlyConfig,
@@ -163,7 +165,8 @@ module SecureHeaders
163
165
  # returned is meant to be merged into the header value from `@app.call(env)`
164
166
  # in Rack middleware.
165
167
  def header_hash_for(request)
166
- config = config_for(request, prevent_dup = true)
168
+ prevent_dup = true
169
+ config = config_for(request, prevent_dup)
167
170
  headers = config.cached_headers
168
171
  user_agent = UserAgent.parse(request.user_agent)
169
172
 
@@ -177,7 +180,7 @@ module SecureHeaders
177
180
 
178
181
  header_classes_for(request).each_with_object({}) do |klass, hash|
179
182
  if header = headers[klass::CONFIG_KEY]
180
- header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass)
183
+ header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
181
184
  csp_header_for_ua(header, user_agent)
182
185
  else
183
186
  header
@@ -1,10 +1,10 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |gem|
3
3
  gem.name = "secure_headers"
4
- gem.version = "3.6.0"
4
+ gem.version = "3.7.0"
5
5
  gem.authors = ["Neil Matatall"]
6
6
  gem.email = ["neil.matatall@gmail.com"]
7
- gem.description = 'Security related headers all in one gem.'
7
+ gem.description = 'Manages application of security headers with many safe defaults.'
8
8
  gem.summary = 'Add easily configured security headers to responses
9
9
  including content-security-policy, x-frame-options,
10
10
  strict-transport-security, etc.'
@@ -19,30 +19,16 @@ module SecureHeaders
19
19
  name, value = described_class.make_header(true)
20
20
 
21
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
22
+ expect(value).to eq(
23
+ %("cache", "cookies", "storage", "executionContexts")
24
+ )
32
25
  end
33
26
 
34
27
  it "returns specified types" do
35
28
  name, value = described_class.make_header(["foo", "bar"])
36
29
 
37
30
  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
31
+ expect(value).to eq(%("foo", "bar"))
46
32
  end
47
33
  end
48
34
 
@@ -83,12 +69,6 @@ module SecureHeaders
83
69
  end.to raise_error(ClearSiteDataConfigError)
84
70
  end
85
71
 
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
72
  it "fails for other types of config" do
93
73
  expect do
94
74
  described_class.validate_config!(:cookies)
@@ -96,8 +76,11 @@ module SecureHeaders
96
76
  end
97
77
  end
98
78
 
99
- def normalize_json(json)
100
- JSON.dump(JSON.parse(json))
79
+ describe "make_header_value" do
80
+ it "returns a string of quoted values that are comma separated" do
81
+ value = described_class.make_header_value(["foo", "bar"])
82
+ expect(value).to eq(%("foo", "bar"))
83
+ end
101
84
  end
102
85
  end
103
86
  end
@@ -86,8 +86,8 @@ module SecureHeaders
86
86
  expect(csp.value).to eq("default-src example.org")
87
87
  end
88
88
 
89
- it "emits a warning when using frame-src" do
90
- expect(Kernel).to receive(:warn).with(/:frame_src is deprecated, use :child_src instead./)
89
+ it "does not emit a warning when using frame-src" do
90
+ expect(Kernel).to_not receive(:warn)
91
91
  ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
92
92
  end
93
93
 
@@ -119,7 +119,6 @@ module SecureHeaders
119
119
  end.merge({
120
120
  block_all_mixed_content: true,
121
121
  upgrade_insecure_requests: true,
122
- reflected_xss: "block",
123
122
  script_src: %w(script-src.com),
124
123
  script_nonce: 123456
125
124
  })
@@ -127,22 +126,22 @@ module SecureHeaders
127
126
 
128
127
  it "does not filter any directives for Chrome" do
129
128
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
130
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
129
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
131
130
  end
132
131
 
133
132
  it "does not filter any directives for Opera" do
134
133
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
135
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
134
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
136
135
  end
137
136
 
138
137
  it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
139
138
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
140
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
139
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
141
140
  end
142
141
 
143
142
  it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
144
143
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
145
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
144
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
146
145
  end
147
146
 
148
147
  it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
@@ -155,11 +154,16 @@ module SecureHeaders
155
154
  expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
156
155
  end
157
156
 
157
+ it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
158
+ policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
159
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
160
+ end
161
+
158
162
  it "falls back to standard Firefox defaults when the useragent version is not present" do
159
163
  ua = USER_AGENTS[:firefox].dup
160
164
  allow(ua).to receive(:version).and_return(nil)
161
165
  policy = ContentSecurityPolicy.new(complex_opts, ua)
162
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
166
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
163
167
  end
164
168
  end
165
169
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ module SecureHeaders
5
+ describe ExpectCertificateTransparency do
6
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce; max-age=1234") }
7
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") }
8
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") }
9
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"") }
10
+ specify do
11
+ config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" }
12
+ header_value = "enforce; max-age=1234; report-uri=\"https://report-uri.io/expect-ct\""
13
+ expect(ExpectCertificateTransparency.new(config).value).to eq(header_value)
14
+ end
15
+
16
+ context "with an invalid configuration" do
17
+ it "raises an exception when configuration isn't a hash" do
18
+ expect do
19
+ ExpectCertificateTransparency.validate_config!(%w(a))
20
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
21
+ end
22
+
23
+ it "raises an exception when max-age is not provided" do
24
+ expect do
25
+ ExpectCertificateTransparency.validate_config!(foo: "bar")
26
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
27
+ end
28
+
29
+ it "raises an exception with an invalid max-age" do
30
+ expect do
31
+ ExpectCertificateTransparency.validate_config!(max_age: "abc123")
32
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
33
+ end
34
+
35
+ it "raises an exception with an invalid enforce value" do
36
+ expect do
37
+ ExpectCertificateTransparency.validate_config!(enforce: "brokenstring")
38
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -27,6 +27,7 @@ module SecureHeaders
27
27
  connect_src: %w(wss:),
28
28
  font_src: %w('self' data:),
29
29
  img_src: %w(mycdn.com data:),
30
+ manifest_src: %w(manifest.com),
30
31
  media_src: %w(utoob.com),
31
32
  object_src: %w('self'),
32
33
  script_src: %w('self'),
@@ -37,7 +37,6 @@ class Message < ERB
37
37
  background-color: black;
38
38
  }
39
39
  </style>
40
- <%= @name %>
41
40
 
42
41
  TEMPLATE
43
42
  end