secure_headers 3.7.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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -6
- data/CHANGELOG.md +2 -10
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +7 -4
- data/Guardfile +1 -0
- data/README.md +4 -24
- data/Rakefile +22 -18
- data/docs/cookies.md +16 -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 +7 -12
- data/lib/secure_headers/headers/content_security_policy_config.rb +1 -0
- data/lib/secure_headers/headers/cookie.rb +21 -3
- data/lib/secure_headers/headers/policy_management.rb +10 -2
- 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 +13 -12
- data/lib/secure_headers/view_helper.rb +2 -1
- data/lib/secure_headers.rb +1 -2
- 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 +11 -16
- data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +20 -10
- 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 +15 -11
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +0 -70
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +0 -42
|
@@ -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!
|
|
@@ -51,18 +53,18 @@ module SecureHeaders
|
|
|
51
53
|
def validate_samesite_hash_config!
|
|
52
54
|
# validate Hash-based samesite configuration
|
|
53
55
|
if is_hash?(config[:samesite][:lax])
|
|
54
|
-
validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax],
|
|
56
|
+
validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], "samesite lax")
|
|
55
57
|
|
|
56
58
|
if is_hash?(config[:samesite][:strict])
|
|
57
|
-
validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict],
|
|
59
|
+
validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], "samesite strict")
|
|
58
60
|
validate_exclusive_use_of_samesite_enforcement!(:only)
|
|
59
61
|
validate_exclusive_use_of_samesite_enforcement!(:except)
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
def
|
|
65
|
-
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]))
|
|
66
68
|
raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
|
|
67
69
|
end
|
|
68
70
|
end
|
|
@@ -70,7 +72,6 @@ module SecureHeaders
|
|
|
70
72
|
# validate exclusive use of only or except but not both at the same time
|
|
71
73
|
def validate_exclusive_use_of_hash_constraints!(conf, attribute)
|
|
72
74
|
return unless is_hash?(conf)
|
|
73
|
-
|
|
74
75
|
if conf.key?(:only) && conf.key?(:except)
|
|
75
76
|
raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
|
|
76
77
|
end
|
|
@@ -87,8 +88,8 @@ module SecureHeaders
|
|
|
87
88
|
obj && obj.is_a?(Hash)
|
|
88
89
|
end
|
|
89
90
|
|
|
90
|
-
def
|
|
91
|
-
obj && (obj.is_a?(TrueClass) || obj
|
|
91
|
+
def is_true_or_opt_out?(obj)
|
|
92
|
+
obj && (obj.is_a?(TrueClass) || obj == OPT_OUT)
|
|
92
93
|
end
|
|
93
94
|
end
|
|
94
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/secure_headers.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require "secure_headers/configuration"
|
|
2
3
|
require "secure_headers/hash_helper"
|
|
3
4
|
require "secure_headers/headers/cookie"
|
|
@@ -11,7 +12,6 @@ require "secure_headers/headers/x_download_options"
|
|
|
11
12
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
12
13
|
require "secure_headers/headers/referrer_policy"
|
|
13
14
|
require "secure_headers/headers/clear_site_data"
|
|
14
|
-
require "secure_headers/headers/expect_certificate_transparency"
|
|
15
15
|
require "secure_headers/middleware"
|
|
16
16
|
require "secure_headers/railtie"
|
|
17
17
|
require "secure_headers/view_helper"
|
|
@@ -52,7 +52,6 @@ module SecureHeaders
|
|
|
52
52
|
CSP = ContentSecurityPolicy
|
|
53
53
|
|
|
54
54
|
ALL_HEADER_CLASSES = [
|
|
55
|
-
ExpectCertificateTransparency,
|
|
56
55
|
ClearSiteData,
|
|
57
56
|
ContentSecurityPolicyConfig,
|
|
58
57
|
ContentSecurityPolicyReportOnlyConfig,
|
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
|
|
@@ -23,6 +24,10 @@ module SecureHeaders
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
describe "#value" do
|
|
27
|
+
it "uses a safe but non-breaking default value" do
|
|
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:")
|
|
29
|
+
end
|
|
30
|
+
|
|
26
31
|
it "discards 'none' values if any other source expressions are present" do
|
|
27
32
|
csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
|
|
28
33
|
expect(csp.value).not_to include("'none'")
|
|
@@ -52,7 +57,7 @@ module SecureHeaders
|
|
|
52
57
|
end
|
|
53
58
|
|
|
54
59
|
it "does not remove schemes when :preserve_schemes is true" do
|
|
55
|
-
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :
|
|
60
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), preserve_schemes: true)
|
|
56
61
|
expect(csp.value).to eq("default-src https://example.org")
|
|
57
62
|
end
|
|
58
63
|
|
|
@@ -91,20 +96,10 @@ module SecureHeaders
|
|
|
91
96
|
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
92
97
|
end
|
|
93
98
|
|
|
94
|
-
it "
|
|
95
|
-
expect
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
it "will still set inconsistent child/frame-src values to be less surprising" do
|
|
100
|
-
expect(Kernel).to receive(:warn).at_least(:once)
|
|
101
|
-
firefox = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox]).value
|
|
102
|
-
firefox_transitional = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox46]).value
|
|
103
|
-
expect(firefox).not_to eq(firefox_transitional)
|
|
104
|
-
expect(firefox).to match(/frame-src/)
|
|
105
|
-
expect(firefox).not_to match(/child-src/)
|
|
106
|
-
expect(firefox_transitional).to match(/child-src/)
|
|
107
|
-
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)
|
|
108
103
|
end
|
|
109
104
|
|
|
110
105
|
it "supports strict-dynamic" do
|
|
@@ -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,19 +49,19 @@ 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
|
|
@@ -63,46 +69,52 @@ module SecureHeaders
|
|
|
63
69
|
|
|
64
70
|
context "SameSite cookies" do
|
|
65
71
|
it "flags SameSite=Lax" do
|
|
66
|
-
cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } })
|
|
72
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT)
|
|
67
73
|
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
it "flags SameSite=Lax when configured with a boolean" do
|
|
71
|
-
cookie = Cookie.new(raw_cookie, samesite: { lax: true})
|
|
77
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: true}, secure: OPT_OUT, httponly: OPT_OUT)
|
|
72
78
|
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
it "does not flag cookies as SameSite=Lax when excluded" do
|
|
76
|
-
cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } })
|
|
82
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT)
|
|
77
83
|
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
78
84
|
end
|
|
79
85
|
|
|
80
86
|
it "flags SameSite=Strict" do
|
|
81
|
-
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } })
|
|
87
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT)
|
|
82
88
|
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
83
89
|
end
|
|
84
90
|
|
|
85
91
|
it "does not flag cookies as SameSite=Strict when excluded" do
|
|
86
|
-
cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] }
|
|
92
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] }}, secure: OPT_OUT, httponly: OPT_OUT)
|
|
87
93
|
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
88
94
|
end
|
|
89
95
|
|
|
90
96
|
it "flags SameSite=Strict when configured with a boolean" do
|
|
91
|
-
cookie = Cookie.new(raw_cookie, samesite: { strict: true})
|
|
97
|
+
cookie = Cookie.new(raw_cookie, {samesite: { strict: true}, secure: OPT_OUT, httponly: OPT_OUT})
|
|
92
98
|
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
93
99
|
end
|
|
94
100
|
|
|
95
101
|
it "flags properly when both lax and strict are configured" do
|
|
96
102
|
raw_cookie = "_session=thisisatest"
|
|
97
|
-
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)
|
|
98
104
|
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
99
105
|
end
|
|
100
106
|
|
|
101
107
|
it "ignores configuration if the cookie is already flagged" do
|
|
102
108
|
raw_cookie = "_session=thisisatest; SameSite=Strict"
|
|
103
|
-
cookie = Cookie.new(raw_cookie, samesite: { lax: true })
|
|
109
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: true }, secure: OPT_OUT, httponly: OPT_OUT)
|
|
104
110
|
expect(cookie.to_s).to eq(raw_cookie)
|
|
105
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
|
|
106
118
|
end
|
|
107
119
|
end
|
|
108
120
|
|
|
@@ -113,12 +125,18 @@ module SecureHeaders
|
|
|
113
125
|
end.to raise_error(CookiesConfigError)
|
|
114
126
|
end
|
|
115
127
|
|
|
116
|
-
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
|
|
117
129
|
expect do
|
|
118
130
|
Cookie.validate_config!(secure: "true")
|
|
119
131
|
end.to raise_error(CookiesConfigError)
|
|
120
132
|
end
|
|
121
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
|
+
|
|
122
140
|
it "raises an exception when both only and except filters are provided" do
|
|
123
141
|
expect do
|
|
124
142
|
Cookie.validate_config!(secure: { only: [], except: [] })
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe PolicyManagement do
|
|
@@ -16,7 +17,7 @@ module SecureHeaders
|
|
|
16
17
|
it "accepts all keys" do
|
|
17
18
|
# (pulled from README)
|
|
18
19
|
config = {
|
|
19
|
-
# "meta" values. these will
|
|
20
|
+
# "meta" values. these will shape the header, but the values are not included in the header.
|
|
20
21
|
report_only: true, # default: false
|
|
21
22
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
22
23
|
|
|
@@ -46,7 +47,13 @@ module SecureHeaders
|
|
|
46
47
|
|
|
47
48
|
it "requires a :default_src value" do
|
|
48
49
|
expect do
|
|
49
|
-
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self')))
|
|
50
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %w('self')))
|
|
51
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "requires a :script_src value" do
|
|
55
|
+
expect do
|
|
56
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self')))
|
|
50
57
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
51
58
|
end
|
|
52
59
|
|
|
@@ -95,7 +102,7 @@ module SecureHeaders
|
|
|
95
102
|
# this is mostly to ensure people don't use the antiquated shorthands common in other configs
|
|
96
103
|
it "performs light validation on source lists" do
|
|
97
104
|
expect do
|
|
98
|
-
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval)))
|
|
105
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval), script_src: %w('self')))
|
|
99
106
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
100
107
|
end
|
|
101
108
|
end
|
|
@@ -104,19 +111,21 @@ module SecureHeaders
|
|
|
104
111
|
it "combines the default-src value with the override if the directive was unconfigured" do
|
|
105
112
|
Configuration.default do |config|
|
|
106
113
|
config.csp = {
|
|
107
|
-
default_src: %w(https:)
|
|
114
|
+
default_src: %w(https:),
|
|
115
|
+
script_src: %w('self'),
|
|
108
116
|
}
|
|
109
117
|
end
|
|
110
|
-
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h,
|
|
118
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, style_src: %w(anothercdn.com))
|
|
111
119
|
csp = ContentSecurityPolicy.new(combined_config)
|
|
112
120
|
expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME)
|
|
113
|
-
expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
|
|
121
|
+
expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com")
|
|
114
122
|
end
|
|
115
123
|
|
|
116
124
|
it "combines directives where the original value is nil and the hash is frozen" do
|
|
117
125
|
Configuration.default do |config|
|
|
118
126
|
config.csp = {
|
|
119
127
|
default_src: %w('self'),
|
|
128
|
+
script_src: %w('self'),
|
|
120
129
|
report_only: false
|
|
121
130
|
}.freeze
|
|
122
131
|
end
|
|
@@ -130,6 +139,7 @@ module SecureHeaders
|
|
|
130
139
|
Configuration.default do |config|
|
|
131
140
|
config.csp = {
|
|
132
141
|
default_src: %w('self'),
|
|
142
|
+
script_src: %w('self'),
|
|
133
143
|
report_only: false
|
|
134
144
|
}.freeze
|
|
135
145
|
end
|
|
@@ -141,14 +151,13 @@ module SecureHeaders
|
|
|
141
151
|
ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive|
|
|
142
152
|
expect(combined_config[directive]).to eq(%w("http://example.org))
|
|
143
153
|
end
|
|
144
|
-
|
|
145
|
-
ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value
|
|
146
154
|
end
|
|
147
155
|
|
|
148
156
|
it "overrides the report_only flag" do
|
|
149
157
|
Configuration.default do |config|
|
|
150
158
|
config.csp = {
|
|
151
159
|
default_src: %w('self'),
|
|
160
|
+
script_src: %w('self'),
|
|
152
161
|
report_only: false
|
|
153
162
|
}
|
|
154
163
|
end
|
|
@@ -161,12 +170,13 @@ module SecureHeaders
|
|
|
161
170
|
Configuration.default do |config|
|
|
162
171
|
config.csp = {
|
|
163
172
|
default_src: %w(https:),
|
|
173
|
+
script_src: %w('self'),
|
|
164
174
|
block_all_mixed_content: false
|
|
165
175
|
}
|
|
166
176
|
end
|
|
167
177
|
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true)
|
|
168
178
|
csp = ContentSecurityPolicy.new(combined_config)
|
|
169
|
-
expect(csp.value).to eq("default-src https:; block-all-mixed-content")
|
|
179
|
+
expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'")
|
|
170
180
|
end
|
|
171
181
|
|
|
172
182
|
it "raises an error if appending to a OPT_OUT policy" do
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe PublicKeyPins do
|
|
@@ -8,7 +9,7 @@ module SecureHeaders
|
|
|
8
9
|
specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
|
|
9
10
|
specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
|
|
10
11
|
specify do
|
|
11
|
-
config = { max_age: 1234, pins: [{ sha256:
|
|
12
|
+
config = { max_age: 1234, pins: [{ sha256: "base64encodedpin1" }, { sha256: "base64encodedpin2" }] }
|
|
12
13
|
header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\""
|
|
13
14
|
expect(PublicKeyPins.new(config).value).to eq(header_value)
|
|
14
15
|
end
|
|
@@ -16,19 +17,19 @@ module SecureHeaders
|
|
|
16
17
|
context "with an invalid configuration" do
|
|
17
18
|
it "raises an exception when max-age is not provided" do
|
|
18
19
|
expect do
|
|
19
|
-
PublicKeyPins.validate_config!(foo:
|
|
20
|
+
PublicKeyPins.validate_config!(foo: "bar")
|
|
20
21
|
end.to raise_error(PublicKeyPinsConfigError)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
it "raises an exception with an invalid max-age" do
|
|
24
25
|
expect do
|
|
25
|
-
PublicKeyPins.validate_config!(max_age:
|
|
26
|
+
PublicKeyPins.validate_config!(max_age: "abc123")
|
|
26
27
|
end.to raise_error(PublicKeyPinsConfigError)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
it
|
|
30
|
+
it "raises an exception with less than 2 pins" do
|
|
30
31
|
expect do
|
|
31
|
-
config = { max_age: 1234, pins: [{ sha256:
|
|
32
|
+
config = { max_age: 1234, pins: [{ sha256: "base64encodedpin" }] }
|
|
32
33
|
PublicKeyPins.validate_config!(config)
|
|
33
34
|
end.to raise_error(PublicKeyPinsConfigError)
|
|
34
35
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe ReferrerPolicy do
|
|
5
6
|
specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) }
|
|
6
|
-
specify { expect(ReferrerPolicy.make_header(
|
|
7
|
+
specify { expect(ReferrerPolicy.make_header("no-referrer")).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) }
|
|
7
8
|
|
|
8
9
|
context "valid configuration values" do
|
|
9
10
|
it "accepts 'no-referrer'" do
|
|
@@ -61,7 +62,7 @@ module SecureHeaders
|
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
context
|
|
65
|
+
context "invlaid configuration values" do
|
|
65
66
|
it "doesn't accept invalid values" do
|
|
66
67
|
expect do
|
|
67
68
|
ReferrerPolicy.validate_config!("open")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe StrictTransportSecurity do
|
|
@@ -10,19 +11,19 @@ module SecureHeaders
|
|
|
10
11
|
context "with a string argument" do
|
|
11
12
|
it "raises an exception with an invalid max-age" do
|
|
12
13
|
expect do
|
|
13
|
-
StrictTransportSecurity.validate_config!(
|
|
14
|
+
StrictTransportSecurity.validate_config!("max-age=abc123")
|
|
14
15
|
end.to raise_error(STSConfigError)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
it "raises an exception if max-age is not supplied" do
|
|
18
19
|
expect do
|
|
19
|
-
StrictTransportSecurity.validate_config!(
|
|
20
|
+
StrictTransportSecurity.validate_config!("includeSubdomains")
|
|
20
21
|
end.to raise_error(STSConfigError)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
it "raises an exception with an invalid format" do
|
|
24
25
|
expect do
|
|
25
|
-
StrictTransportSecurity.validate_config!(
|
|
26
|
+
StrictTransportSecurity.validate_config!("max-age=123includeSubdomains")
|
|
26
27
|
end.to raise_error(STSConfigError)
|
|
27
28
|
end
|
|
28
29
|
end
|