secure_headers 3.9.0 → 4.0.0.alpha01

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +8 -6
  6. data/CHANGELOG.md +2 -34
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +7 -4
  9. data/Guardfile +1 -0
  10. data/README.md +4 -25
  11. data/Rakefile +22 -18
  12. data/docs/cookies.md +18 -5
  13. data/lib/secure_headers.rb +1 -2
  14. data/lib/secure_headers/configuration.rb +6 -16
  15. data/lib/secure_headers/hash_helper.rb +2 -1
  16. data/lib/secure_headers/headers/clear_site_data.rb +2 -1
  17. data/lib/secure_headers/headers/content_security_policy.rb +14 -60
  18. data/lib/secure_headers/headers/content_security_policy_config.rb +1 -1
  19. data/lib/secure_headers/headers/cookie.rb +22 -10
  20. data/lib/secure_headers/headers/policy_management.rb +57 -98
  21. data/lib/secure_headers/headers/public_key_pins.rb +4 -3
  22. data/lib/secure_headers/headers/referrer_policy.rb +1 -0
  23. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  24. data/lib/secure_headers/headers/x_content_type_options.rb +1 -0
  25. data/lib/secure_headers/headers/x_download_options.rb +2 -1
  26. data/lib/secure_headers/headers/x_frame_options.rb +1 -0
  27. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -1
  28. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  29. data/lib/secure_headers/middleware.rb +10 -9
  30. data/lib/secure_headers/railtie.rb +7 -6
  31. data/lib/secure_headers/utils/cookies_config.rb +17 -18
  32. data/lib/secure_headers/view_helper.rb +2 -1
  33. data/lib/tasks/tasks.rake +2 -1
  34. data/secure_headers.gemspec +13 -3
  35. data/spec/lib/secure_headers/configuration_spec.rb +9 -8
  36. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +2 -1
  37. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +17 -53
  38. data/spec/lib/secure_headers/headers/cookie_spec.rb +58 -37
  39. data/spec/lib/secure_headers/headers/policy_management_spec.rb +20 -41
  40. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  41. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +4 -3
  42. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  43. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  44. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  45. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  46. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  47. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  48. data/spec/lib/secure_headers/middleware_spec.rb +18 -21
  49. data/spec/lib/secure_headers/view_helpers_spec.rb +5 -4
  50. data/spec/lib/secure_headers_spec.rb +92 -120
  51. data/spec/spec_helper.rb +9 -23
  52. data/upgrading-to-4-0.md +49 -0
  53. metadata +16 -11
  54. data/lib/secure_headers/headers/expect_certificate_transparency.rb +0 -70
  55. data/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb +0 -42
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class Middleware
3
4
  HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166"
@@ -25,11 +26,11 @@ module SecureHeaders
25
26
 
26
27
  # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
27
28
  def flag_cookies!(headers, config)
28
- if cookies = headers['Set-Cookie']
29
+ if cookies = headers["Set-Cookie"]
29
30
  # Support Rails 2.3 / Rack 1.1 arrays as headers
30
31
  cookies = cookies.split("\n") unless cookies.is_a?(Array)
31
32
 
32
- headers['Set-Cookie'] = cookies.map do |cookie|
33
+ headers["Set-Cookie"] = cookies.map do |cookie|
33
34
  SecureHeaders::Cookie.new(cookie, config).to_s
34
35
  end.join("\n")
35
36
  end
@@ -37,8 +38,8 @@ module SecureHeaders
37
38
 
38
39
  # disable Secure cookies for non-https requests
39
40
  def override_secure(env, config = {})
40
- if scheme(env) != 'https'
41
- config.merge!(secure: false)
41
+ if scheme(env) != "https"
42
+ config[:secure] = OPT_OUT
42
43
  end
43
44
 
44
45
  config
@@ -46,12 +47,12 @@ module SecureHeaders
46
47
 
47
48
  # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
48
49
  def scheme(env)
49
- if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on'
50
- 'https'
51
- elsif env['HTTP_X_FORWARDED_PROTO']
52
- env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
50
+ if env["HTTPS"] == "on" || env["HTTP_X_SSL_REQUEST"] == "on"
51
+ "https"
52
+ elsif env["HTTP_X_FORWARDED_PROTO"]
53
+ env["HTTP_X_FORWARDED_PROTO"].split(",")[0]
53
54
  else
54
- env['rack.url_scheme']
55
+ env["rack.url_scheme"]
55
56
  end
56
57
  end
57
58
  end
@@ -1,20 +1,21 @@
1
+ # frozen_string_literal: true
1
2
  # rails 3.1+
2
3
  if defined?(Rails::Railtie)
3
4
  module SecureHeaders
4
5
  class Railtie < Rails::Railtie
5
6
  isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0
6
- conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection',
7
- 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
8
- 'X-Content-Type-Options', 'Strict-Transport-Security',
9
- 'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
10
- 'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy']
7
+ conflicting_headers = ["X-Frame-Options", "X-XSS-Protection",
8
+ "X-Permitted-Cross-Domain-Policies", "X-Download-Options",
9
+ "X-Content-Type-Options", "Strict-Transport-Security",
10
+ "Content-Security-Policy", "Content-Security-Policy-Report-Only",
11
+ "Public-Key-Pins", "Public-Key-Pins-Report-Only", "Referrer-Policy"]
11
12
 
12
13
  initializer "secure_headers.middleware" do
13
14
  Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
14
15
  end
15
16
 
16
17
  rake_tasks do
17
- load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__))
18
+ load File.expand_path(File.join("..", "..", "lib", "tasks", "tasks.rake"), File.dirname(__FILE__))
18
19
  end
19
20
 
20
21
  initializer "secure_headers.action_controller" do
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class CookiesConfig
3
4
 
@@ -11,9 +12,9 @@ module SecureHeaders
11
12
  return if config.nil? || config == SecureHeaders::OPT_OUT
12
13
 
13
14
  validate_config!
14
- validate_secure_config! if config[:secure]
15
- validate_httponly_config! if config[:httponly]
16
- validate_samesite_config! if config[:samesite]
15
+ validate_secure_config! unless config[:secure].nil?
16
+ validate_httponly_config! unless config[:httponly].nil?
17
+ validate_samesite_config! unless config[:samesite].nil?
17
18
  end
18
19
 
19
20
  private
@@ -23,16 +24,17 @@ module SecureHeaders
23
24
  end
24
25
 
25
26
  def validate_secure_config!
26
- validate_hash_or_boolean!(:secure)
27
+ validate_hash_or_true_or_opt_out!(:secure)
27
28
  validate_exclusive_use_of_hash_constraints!(config[:secure], :secure)
28
29
  end
29
30
 
30
31
  def validate_httponly_config!
31
- validate_hash_or_boolean!(:httponly)
32
+ validate_hash_or_true_or_opt_out!(:httponly)
32
33
  validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly)
33
34
  end
34
35
 
35
36
  def validate_samesite_config!
37
+ return if config[:samesite] == OPT_OUT
36
38
  raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite])
37
39
 
38
40
  validate_samesite_boolean_config!
@@ -41,30 +43,28 @@ module SecureHeaders
41
43
 
42
44
  # when configuring with booleans, only one enforcement is permitted
43
45
  def validate_samesite_boolean_config!
44
- if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && (config[:samesite].key?(:strict) || config[:samesite].key?(:none))
45
- raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax with strict or no enforcement is not permitted.")
46
- elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:none))
47
- raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure strict with lax or no enforcement is not permitted.")
48
- elsif config[:samesite].key?(:none) && config[:samesite][:none].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:strict))
49
- raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure no enforcement with lax or strict is not permitted.")
46
+ if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
47
+ raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
48
+ elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
49
+ raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
50
50
  end
51
51
  end
52
52
 
53
53
  def validate_samesite_hash_config!
54
54
  # validate Hash-based samesite configuration
55
55
  if is_hash?(config[:samesite][:lax])
56
- validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], 'samesite lax')
56
+ validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], "samesite lax")
57
57
 
58
58
  if is_hash?(config[:samesite][:strict])
59
- validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict')
59
+ validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], "samesite strict")
60
60
  validate_exclusive_use_of_samesite_enforcement!(:only)
61
61
  validate_exclusive_use_of_samesite_enforcement!(:except)
62
62
  end
63
63
  end
64
64
  end
65
65
 
66
- def validate_hash_or_boolean!(attribute)
67
- if !(is_hash?(config[attribute]) || is_boolean?(config[attribute]))
66
+ def validate_hash_or_true_or_opt_out!(attribute)
67
+ if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute]))
68
68
  raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
69
69
  end
70
70
  end
@@ -72,7 +72,6 @@ module SecureHeaders
72
72
  # validate exclusive use of only or except but not both at the same time
73
73
  def validate_exclusive_use_of_hash_constraints!(conf, attribute)
74
74
  return unless is_hash?(conf)
75
-
76
75
  if conf.key?(:only) && conf.key?(:except)
77
76
  raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
78
77
  end
@@ -89,8 +88,8 @@ module SecureHeaders
89
88
  obj && obj.is_a?(Hash)
90
89
  end
91
90
 
92
- def is_boolean?(obj)
93
- obj && (obj.is_a?(TrueClass) || obj.is_a?(FalseClass))
91
+ def is_true_or_opt_out?(obj)
92
+ obj && (obj.is_a?(TrueClass) || obj == OPT_OUT)
94
93
  end
95
94
  end
96
95
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  module ViewHelpers
3
4
  include SecureHeaders::HashHelper
@@ -75,7 +76,7 @@ module SecureHeaders
75
76
  end
76
77
 
77
78
  content = capture(&block)
78
- file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb')
79
+ file_path = File.join("app", "views", self.instance_variable_get(:@virtual_path) + ".html.erb")
79
80
 
80
81
  if raise_error_on_unrecognized_hash
81
82
  hash_value = hash_source(content)
data/lib/tasks/tasks.rake CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
2
3
  INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
3
4
  INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
@@ -73,7 +74,7 @@ namespace :secure_headers do
73
74
  end
74
75
  end
75
76
 
76
- File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file|
77
+ File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, "w") do |file|
77
78
  file.write(script_hashes.to_yaml)
78
79
  end
79
80
 
@@ -1,10 +1,11 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
2
3
  Gem::Specification.new do |gem|
3
4
  gem.name = "secure_headers"
4
- gem.version = "3.9.0"
5
+ gem.version = "4.0.0.alpha01"
5
6
  gem.authors = ["Neil Matatall"]
6
7
  gem.email = ["neil.matatall@gmail.com"]
7
- gem.description = 'Manages application of security headers with many safe defaults.'
8
+ gem.description = "Manages application of security headers with many safe defaults."
8
9
  gem.summary = 'Add easily configured security headers to responses
9
10
  including content-security-policy, x-frame-options,
10
11
  strict-transport-security, etc.'
@@ -15,5 +16,14 @@ Gem::Specification.new do |gem|
15
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
17
  gem.require_paths = ["lib"]
17
18
  gem.add_development_dependency "rake"
18
- gem.add_dependency "useragent"
19
+ gem.add_dependency "useragent", ">= 0.15.0"
20
+
21
+ # TODO: delete this after 4.1 is cut or a number of 4.0.x releases have occurred
22
+ gem.post_install_message = <<-POST_INSTALL
23
+
24
+ **********
25
+ :wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md
26
+ **********
27
+
28
+ POST_INSTALL
19
29
  end
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe Configuration do
@@ -70,7 +71,7 @@ module SecureHeaders
70
71
 
71
72
  it "allows you to override an override" do
72
73
  Configuration.override(:override) do |config|
73
- config.csp = { default_src: %w('self')}
74
+ config.csp = { default_src: %w('self'), script_src: %w('self')}
74
75
  end
75
76
 
76
77
  Configuration.override(:second_override, :override) do |config|
@@ -78,17 +79,17 @@ module SecureHeaders
78
79
  end
79
80
 
80
81
  original_override = Configuration.get(:override)
81
- expect(original_override.csp.to_h).to eq(default_src: %w('self'))
82
+ expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
82
83
  override_config = Configuration.get(:second_override)
83
84
  expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
84
85
  end
85
86
 
86
87
  it "deprecates the secure_cookies configuration" do
87
- expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/)
88
-
89
- Configuration.default do |config|
90
- config.secure_cookies = true
91
- end
88
+ expect {
89
+ Configuration.default do |config|
90
+ config.secure_cookies = true
91
+ end
92
+ }.to raise_error(ArgumentError)
92
93
  end
93
94
  end
94
95
  end
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe ClearSiteData do
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe ContentSecurityPolicy do
@@ -24,17 +25,7 @@ module SecureHeaders
24
25
 
25
26
  describe "#value" do
26
27
  it "uses a safe but non-breaking default value" do
27
- expect(ContentSecurityPolicy.new.value).to eq("default-src https:")
28
- end
29
-
30
- it "deprecates and escapes semicolons in directive source lists" do
31
- expect(Kernel).to receive(:warn).with(%(frame_ancestors contains a ; in "google.com;script-src *;.;" which will raise an error in future versions. It has been replaced with a blank space.))
32
- expect(ContentSecurityPolicy.new(frame_ancestors: %w(https://google.com;script-src https://*;.;)).value).to eq("frame-ancestors google.com script-src * .")
33
- end
34
-
35
- it "deprecates and escapes semicolons in directive source lists" do
36
- expect(Kernel).to receive(:warn).with(%(frame_ancestors contains a \n in "\\nfoo.com\\nhacked" which will raise an error in future versions. It has been replaced with a blank space.))
37
- expect(ContentSecurityPolicy.new(frame_ancestors: ["\nfoo.com\nhacked"]).value).to eq("frame-ancestors foo.com hacked")
28
+ expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:")
38
29
  end
39
30
 
40
31
  it "discards 'none' values if any other source expressions are present" do
@@ -66,7 +57,7 @@ module SecureHeaders
66
57
  end
67
58
 
68
59
  it "does not remove schemes when :preserve_schemes is true" do
69
- csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :preserve_schemes => true)
60
+ csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), preserve_schemes: true)
70
61
  expect(csp.value).to eq("default-src https://example.org")
71
62
  end
72
63
 
@@ -100,40 +91,15 @@ module SecureHeaders
100
91
  expect(csp.value).to eq("default-src example.org")
101
92
  end
102
93
 
103
- it "creates maximally strict sandbox policy when passed no sandbox token values" do
104
- csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: [])
105
- expect(csp.value).to eq("default-src example.org; sandbox")
106
- end
107
-
108
- it "creates maximally strict sandbox policy when passed true" do
109
- csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: true)
110
- expect(csp.value).to eq("default-src example.org; sandbox")
111
- end
112
-
113
- it "creates sandbox policy when passed valid sandbox token values" do
114
- csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: %w(allow-forms allow-scripts))
115
- expect(csp.value).to eq("default-src example.org; sandbox allow-forms allow-scripts")
116
- end
117
-
118
94
  it "does not emit a warning when using frame-src" do
119
95
  expect(Kernel).to_not receive(:warn)
120
96
  ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
121
97
  end
122
98
 
123
- it "emits a warning when child-src and frame-src are supplied but are not equal" do
124
- expect(Kernel).to receive(:warn).with(/both :child_src and :frame_src supplied and do not match./)
125
- ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
126
- end
127
-
128
- it "will still set inconsistent child/frame-src values to be less surprising" do
129
- expect(Kernel).to receive(:warn).at_least(:once)
130
- firefox = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox]).value
131
- firefox_transitional = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox46]).value
132
- expect(firefox).not_to eq(firefox_transitional)
133
- expect(firefox).to match(/frame-src/)
134
- expect(firefox).not_to match(/child-src/)
135
- expect(firefox_transitional).to match(/child-src/)
136
- expect(firefox_transitional).not_to match(/frame-src/)
99
+ it "raises an error when child-src and frame-src are supplied but are not equal" do
100
+ expect {
101
+ ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
102
+ }.to raise_error(ArgumentError)
137
103
  end
138
104
 
139
105
  it "supports strict-dynamic" do
@@ -149,52 +115,50 @@ module SecureHeaders
149
115
  block_all_mixed_content: true,
150
116
  upgrade_insecure_requests: true,
151
117
  script_src: %w(script-src.com),
152
- script_nonce: 123456,
153
- sandbox: %w(allow-forms),
154
- plugin_types: %w(application/pdf)
118
+ script_nonce: 123456
155
119
  })
156
120
  end
157
121
 
158
122
  it "does not filter any directives for Chrome" do
159
123
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
160
- 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 application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
124
+ 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")
161
125
  end
162
126
 
163
127
  it "does not filter any directives for Opera" do
164
128
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
165
- 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 application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; 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")
166
130
  end
167
131
 
168
132
  it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
169
133
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
170
- 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 allow-forms; 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; 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")
171
135
  end
172
136
 
173
137
  it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
174
138
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
175
- 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 allow-forms; 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; 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")
176
140
  end
177
141
 
178
142
  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
179
143
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
180
- 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 allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
144
+ 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")
181
145
  end
182
146
 
183
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 safari" do
184
148
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
185
- 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 allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
149
+ 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")
186
150
  end
187
151
 
188
152
  it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
189
153
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
190
- 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 application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
154
+ 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")
191
155
  end
192
156
 
193
157
  it "falls back to standard Firefox defaults when the useragent version is not present" do
194
158
  ua = USER_AGENTS[:firefox].dup
195
159
  allow(ua).to receive(:version).and_return(nil)
196
160
  policy = ContentSecurityPolicy.new(complex_opts, ua)
197
- 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 allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
161
+ 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")
198
162
  end
199
163
  end
200
164
  end
@@ -1,40 +1,46 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe Cookie do
5
6
  let(:raw_cookie) { "_session=thisisatest" }
6
7
 
7
- it "does not tamper with cookies when unconfigured" do
8
- cookie = Cookie.new(raw_cookie, {})
8
+ it "does not tamper with cookies when using OPT_OUT is used" do
9
+ cookie = Cookie.new(raw_cookie, OPT_OUT)
9
10
  expect(cookie.to_s).to eq(raw_cookie)
10
11
  end
11
12
 
13
+ it "applies httponly, secure, and samesite by default" do
14
+ cookie = Cookie.new(raw_cookie, nil)
15
+ expect(cookie.to_s).to eq("_session=thisisatest; secure; HttpOnly; SameSite=Lax")
16
+ end
17
+
12
18
  it "preserves existing attributes" do
13
- cookie = Cookie.new("_session=thisisatest; secure", secure: true)
19
+ cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: OPT_OUT, samesite: OPT_OUT)
14
20
  expect(cookie.to_s).to eq("_session=thisisatest; secure")
15
21
  end
16
22
 
17
23
  it "prevents duplicate flagging of attributes" do
18
- cookie = Cookie.new("_session=thisisatest; secure", secure: true)
24
+ cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: OPT_OUT)
19
25
  expect(cookie.to_s.scan(/secure/i).count).to eq(1)
20
26
  end
21
27
 
22
28
  context "Secure cookies" do
23
29
  context "when configured with a boolean" do
24
30
  it "flags cookies as Secure" do
25
- cookie = Cookie.new(raw_cookie, secure: true)
31
+ cookie = Cookie.new(raw_cookie, secure: true, httponly: OPT_OUT, samesite: OPT_OUT)
26
32
  expect(cookie.to_s).to eq("_session=thisisatest; secure")
27
33
  end
28
34
  end
29
35
 
30
36
  context "when configured with a Hash" do
31
37
  it "flags cookies as Secure when whitelisted" do
32
- cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]})
38
+ cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}, httponly: OPT_OUT, samesite: OPT_OUT)
33
39
  expect(cookie.to_s).to eq("_session=thisisatest; secure")
34
40
  end
35
41
 
36
42
  it "does not flag cookies as Secure when excluded" do
37
- cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] })
43
+ cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }, httponly: OPT_OUT, samesite: OPT_OUT)
38
44
  expect(cookie.to_s).to eq("_session=thisisatest")
39
45
  end
40
46
  end
@@ -43,58 +49,72 @@ module SecureHeaders
43
49
  context "HttpOnly cookies" do
44
50
  context "when configured with a boolean" do
45
51
  it "flags cookies as HttpOnly" do
46
- cookie = Cookie.new(raw_cookie, httponly: true)
52
+ cookie = Cookie.new(raw_cookie, httponly: true, secure: OPT_OUT, samesite: OPT_OUT)
47
53
  expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
48
54
  end
49
55
  end
50
56
 
51
57
  context "when configured with a Hash" do
52
58
  it "flags cookies as HttpOnly when whitelisted" do
53
- cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]})
59
+ cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}, secure: OPT_OUT, samesite: OPT_OUT)
54
60
  expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
55
61
  end
56
62
 
57
63
  it "does not flag cookies as HttpOnly when excluded" do
58
- cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] })
64
+ cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }, secure: OPT_OUT, samesite: OPT_OUT)
59
65
  expect(cookie.to_s).to eq("_session=thisisatest")
60
66
  end
61
67
  end
62
68
  end
63
69
 
64
70
  context "SameSite cookies" do
65
- %w(None Lax Strict).each do |flag|
66
- it "flags SameSite=#{flag}" do
67
- cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => { only: ["_session"] } })
68
- expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}")
69
- end
71
+ it "flags SameSite=Lax" do
72
+ cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT)
73
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
74
+ end
70
75
 
71
- it "flags SameSite=#{flag} when configured with a boolean" do
72
- cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => true })
73
- expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}")
74
- end
76
+ it "flags SameSite=Lax when configured with a boolean" do
77
+ cookie = Cookie.new(raw_cookie, samesite: { lax: true}, secure: OPT_OUT, httponly: OPT_OUT)
78
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
79
+ end
75
80
 
76
- it "does not flag cookies as SameSite=#{flag} when excluded" do
77
- cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => { except: ["_session"] } })
78
- expect(cookie.to_s).to eq("_session=thisisatest")
79
- end
81
+ it "does not flag cookies as SameSite=Lax when excluded" do
82
+ cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT)
83
+ expect(cookie.to_s).to eq("_session=thisisatest")
84
+ end
85
+
86
+ it "flags SameSite=Strict" do
87
+ cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT)
88
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
89
+ end
90
+
91
+ it "does not flag cookies as SameSite=Strict when excluded" do
92
+ cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] }}, secure: OPT_OUT, httponly: OPT_OUT)
93
+ expect(cookie.to_s).to eq("_session=thisisatest")
80
94
  end
81
95
 
82
96
  it "flags SameSite=Strict when configured with a boolean" do
83
- cookie = Cookie.new(raw_cookie, samesite: { strict: true})
97
+ cookie = Cookie.new(raw_cookie, {samesite: { strict: true}, secure: OPT_OUT, httponly: OPT_OUT})
84
98
  expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
85
99
  end
86
100
 
87
101
  it "flags properly when both lax and strict are configured" do
88
102
  raw_cookie = "_session=thisisatest"
89
- cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } })
103
+ cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }, secure: OPT_OUT, httponly: OPT_OUT)
90
104
  expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
91
105
  end
92
106
 
93
107
  it "ignores configuration if the cookie is already flagged" do
94
108
  raw_cookie = "_session=thisisatest; SameSite=Strict"
95
- cookie = Cookie.new(raw_cookie, samesite: { lax: true })
109
+ cookie = Cookie.new(raw_cookie, samesite: { lax: true }, secure: OPT_OUT, httponly: OPT_OUT)
96
110
  expect(cookie.to_s).to eq(raw_cookie)
97
111
  end
112
+
113
+ it "samesite: true sets all cookies to samesite=lax" do
114
+ raw_cookie = "_session=thisisatest"
115
+ cookie = Cookie.new(raw_cookie, samesite: true, secure: OPT_OUT, httponly: OPT_OUT)
116
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
117
+ end
98
118
  end
99
119
  end
100
120
 
@@ -105,12 +125,18 @@ module SecureHeaders
105
125
  end.to raise_error(CookiesConfigError)
106
126
  end
107
127
 
108
- it "raises an exception when configured without a boolean/Hash" do
128
+ it "raises an exception when configured without a boolean(true or OPT_OUT)/Hash" do
109
129
  expect do
110
130
  Cookie.validate_config!(secure: "true")
111
131
  end.to raise_error(CookiesConfigError)
112
132
  end
113
133
 
134
+ it "raises an exception when configured with false" do
135
+ expect do
136
+ Cookie.validate_config!(secure: false)
137
+ end.to raise_error(CookiesConfigError)
138
+ end
139
+
114
140
  it "raises an exception when both only and except filters are provided" do
115
141
  expect do
116
142
  Cookie.validate_config!(secure: { only: [], except: [] })
@@ -123,15 +149,10 @@ module SecureHeaders
123
149
  end.to raise_error(CookiesConfigError)
124
150
  end
125
151
 
126
- cookie_options = %w(none lax strict).map(&:to_sym)
127
- cookie_options.each do |flag|
128
- (cookie_options - [flag]).each do |other_flag|
129
- it "raises an exception when SameSite #{flag} and #{other_flag} enforcement modes are configured with booleans" do
130
- expect do
131
- Cookie.validate_config!(samesite: { flag => true, other_flag => true})
132
- end.to raise_error(CookiesConfigError)
133
- end
134
- end
152
+ it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
153
+ expect do
154
+ Cookie.validate_config!(samesite: { lax: true, strict: true})
155
+ end.to raise_error(CookiesConfigError)
135
156
  end
136
157
 
137
158
  it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do