secure_headers 3.9.0 → 4.0.0.alpha01
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.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.rspec +1 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -6
- data/CHANGELOG.md +2 -34
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +7 -4
- data/Guardfile +1 -0
- data/README.md +4 -25
- data/Rakefile +22 -18
- data/docs/cookies.md +18 -5
- data/lib/secure_headers.rb +1 -2
- data/lib/secure_headers/configuration.rb +6 -16
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +2 -1
- data/lib/secure_headers/headers/content_security_policy.rb +14 -60
- data/lib/secure_headers/headers/content_security_policy_config.rb +1 -1
- data/lib/secure_headers/headers/cookie.rb +22 -10
- data/lib/secure_headers/headers/policy_management.rb +57 -98
- data/lib/secure_headers/headers/public_key_pins.rb +4 -3
- data/lib/secure_headers/headers/referrer_policy.rb +1 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -0
- data/lib/secure_headers/headers/x_download_options.rb +2 -1
- data/lib/secure_headers/headers/x_frame_options.rb +1 -0
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -1
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/middleware.rb +10 -9
- data/lib/secure_headers/railtie.rb +7 -6
- data/lib/secure_headers/utils/cookies_config.rb +17 -18
- data/lib/secure_headers/view_helper.rb +2 -1
- data/lib/tasks/tasks.rake +2 -1
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +9 -8
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +17 -53
- data/spec/lib/secure_headers/headers/cookie_spec.rb +58 -37
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +20 -41
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
- data/spec/lib/secure_headers/middleware_spec.rb +18 -21
- data/spec/lib/secure_headers/view_helpers_spec.rb +5 -4
- data/spec/lib/secure_headers_spec.rb +92 -120
- data/spec/spec_helper.rb +9 -23
- data/upgrading-to-4-0.md +49 -0
- metadata +16 -11
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +0 -70
- 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[
|
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[
|
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) !=
|
41
|
-
config
|
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[
|
50
|
-
|
51
|
-
elsif env[
|
52
|
-
env[
|
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[
|
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 = [
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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(
|
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!
|
15
|
-
validate_httponly_config!
|
16
|
-
validate_samesite_config!
|
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
|
-
|
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
|
-
|
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) &&
|
45
|
-
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax
|
46
|
-
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) &&
|
47
|
-
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure
|
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],
|
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],
|
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
|
67
|
-
if !(is_hash?(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
|
93
|
-
obj && (obj.is_a?(TrueClass) || obj
|
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(
|
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,
|
77
|
+
File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, "w") do |file|
|
77
78
|
file.write(script_hashes.to_yaml)
|
78
79
|
end
|
79
80
|
|
data/secure_headers.gemspec
CHANGED
@@ -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 = "
|
5
|
+
gem.version = "4.0.0.alpha01"
|
5
6
|
gem.authors = ["Neil Matatall"]
|
6
7
|
gem.email = ["neil.matatall@gmail.com"]
|
7
|
-
gem.description =
|
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
|
-
|
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
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
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), :
|
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 "
|
124
|
-
expect
|
125
|
-
|
126
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|