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.
- 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
|