secure_headers 3.1.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +126 -0
- data/README.md +147 -24
- data/lib/secure_headers/configuration.rb +46 -4
- data/lib/secure_headers/hash_helper.rb +10 -0
- data/lib/secure_headers/headers/cookie.rb +126 -0
- data/lib/secure_headers/headers/policy_management.rb +2 -0
- data/lib/secure_headers/headers/referrer_policy.rb +33 -0
- data/lib/secure_headers/middleware.rb +28 -8
- data/lib/secure_headers/railtie.rb +5 -1
- data/lib/secure_headers/utils/cookies_config.rb +94 -0
- data/lib/secure_headers/view_helper.rb +64 -0
- data/lib/secure_headers.rb +4 -0
- data/lib/tasks/tasks.rake +81 -0
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +9 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +5 -0
- data/spec/lib/secure_headers/headers/cookie_spec.rb +164 -0
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +54 -0
- data/spec/lib/secure_headers/middleware_spec.rb +64 -13
- data/spec/lib/secure_headers/view_helpers_spec.rb +125 -0
- data/spec/lib/secure_headers_spec.rb +23 -0
- data/spec/spec_helper.rb +14 -0
- data/upgrading-to-3-0.md +1 -0
- metadata +13 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class Middleware
|
|
3
|
-
|
|
3
|
+
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"
|
|
4
4
|
|
|
5
5
|
def initialize(app)
|
|
6
6
|
@app = app
|
|
@@ -12,7 +12,11 @@ module SecureHeaders
|
|
|
12
12
|
status, headers, response = @app.call(env)
|
|
13
13
|
|
|
14
14
|
config = SecureHeaders.config_for(req)
|
|
15
|
-
|
|
15
|
+
if config.hpkp_report_host == req.host
|
|
16
|
+
Kernel.warn(HPKP_SAME_HOST_WARNING)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
|
|
16
20
|
headers.merge!(SecureHeaders.header_hash_for(req))
|
|
17
21
|
[status, headers, response]
|
|
18
22
|
end
|
|
@@ -20,19 +24,35 @@ module SecureHeaders
|
|
|
20
24
|
private
|
|
21
25
|
|
|
22
26
|
# inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
|
|
23
|
-
def
|
|
27
|
+
def flag_cookies!(headers, config)
|
|
24
28
|
if cookies = headers['Set-Cookie']
|
|
25
29
|
# Support Rails 2.3 / Rack 1.1 arrays as headers
|
|
26
30
|
cookies = cookies.split("\n") unless cookies.is_a?(Array)
|
|
27
31
|
|
|
28
32
|
headers['Set-Cookie'] = cookies.map do |cookie|
|
|
29
|
-
|
|
30
|
-
"#{cookie}; secure"
|
|
31
|
-
else
|
|
32
|
-
cookie
|
|
33
|
-
end
|
|
33
|
+
SecureHeaders::Cookie.new(cookie, config).to_s
|
|
34
34
|
end.join("\n")
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
# disable Secure cookies for non-https requests
|
|
39
|
+
def override_secure(env, config = {})
|
|
40
|
+
if scheme(env) != 'https'
|
|
41
|
+
config.merge!(secure: false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
config
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
|
|
48
|
+
def scheme(env)
|
|
49
|
+
if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on'
|
|
50
|
+
'https'
|
|
51
|
+
elsif env['HTTP_X_FORWARDED_PROTO']
|
|
52
|
+
env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
|
|
53
|
+
else
|
|
54
|
+
env['rack.url_scheme']
|
|
55
|
+
end
|
|
56
|
+
end
|
|
37
57
|
end
|
|
38
58
|
end
|
|
@@ -7,12 +7,16 @@ if defined?(Rails::Railtie)
|
|
|
7
7
|
'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
|
|
8
8
|
'X-Content-Type-Options', 'Strict-Transport-Security',
|
|
9
9
|
'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
|
|
10
|
-
'Public-Key-Pins', 'Public-Key-Pins-Report-Only']
|
|
10
|
+
'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy']
|
|
11
11
|
|
|
12
12
|
initializer "secure_headers.middleware" do
|
|
13
13
|
Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
rake_tasks do
|
|
17
|
+
load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__))
|
|
18
|
+
end
|
|
19
|
+
|
|
16
20
|
initializer "secure_headers.action_controller" do
|
|
17
21
|
ActiveSupport.on_load(:action_controller) do
|
|
18
22
|
include SecureHeaders
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class CookiesConfig
|
|
3
|
+
|
|
4
|
+
attr_reader :config
|
|
5
|
+
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def validate!
|
|
11
|
+
return if config.nil? || config == SecureHeaders::OPT_OUT
|
|
12
|
+
|
|
13
|
+
validate_config!
|
|
14
|
+
validate_secure_config! if config[:secure]
|
|
15
|
+
validate_httponly_config! if config[:httponly]
|
|
16
|
+
validate_samesite_config! if config[:samesite]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def validate_config!
|
|
22
|
+
raise CookiesConfigError.new("config must be a hash.") unless is_hash?(config)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate_secure_config!
|
|
26
|
+
validate_hash_or_boolean!(:secure)
|
|
27
|
+
validate_exclusive_use_of_hash_constraints!(config[:secure], :secure)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate_httponly_config!
|
|
31
|
+
validate_hash_or_boolean!(:httponly)
|
|
32
|
+
validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_samesite_config!
|
|
36
|
+
raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite])
|
|
37
|
+
|
|
38
|
+
validate_samesite_boolean_config!
|
|
39
|
+
validate_samesite_hash_config!
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# when configuring with booleans, only one enforcement is permitted
|
|
43
|
+
def validate_samesite_boolean_config!
|
|
44
|
+
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
|
|
45
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
|
|
46
|
+
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
|
|
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
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_samesite_hash_config!
|
|
52
|
+
# validate Hash-based samesite configuration
|
|
53
|
+
if is_hash?(config[:samesite][:lax])
|
|
54
|
+
validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], 'samesite lax')
|
|
55
|
+
|
|
56
|
+
if is_hash?(config[:samesite][:strict])
|
|
57
|
+
validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict')
|
|
58
|
+
validate_exclusive_use_of_samesite_enforcement!(:only)
|
|
59
|
+
validate_exclusive_use_of_samesite_enforcement!(:except)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_hash_or_boolean!(attribute)
|
|
65
|
+
if !(is_hash?(config[attribute]) || is_boolean?(config[attribute]))
|
|
66
|
+
raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# validate exclusive use of only or except but not both at the same time
|
|
71
|
+
def validate_exclusive_use_of_hash_constraints!(conf, attribute)
|
|
72
|
+
return unless is_hash?(conf)
|
|
73
|
+
|
|
74
|
+
if conf.key?(:only) && conf.key?(:except)
|
|
75
|
+
raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# validate exclusivity of only and except members within strict and lax
|
|
80
|
+
def validate_exclusive_use_of_samesite_enforcement!(attribute)
|
|
81
|
+
if (intersection = (config[:samesite][:lax].fetch(attribute, []) & config[:samesite][:strict].fetch(attribute, []))).any?
|
|
82
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def is_hash?(obj)
|
|
87
|
+
obj && obj.is_a?(Hash)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def is_boolean?(obj)
|
|
91
|
+
obj && (obj.is_a?(TrueClass) || obj.is_a?(FalseClass))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
module ViewHelpers
|
|
3
|
+
include SecureHeaders::HashHelper
|
|
4
|
+
SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes"
|
|
5
|
+
|
|
6
|
+
class UnexpectedHashedScriptException < StandardError; end
|
|
7
|
+
|
|
3
8
|
# Public: create a style tag using the content security policy nonce.
|
|
4
9
|
# Instructs secure_headers to append a nonce to style/script-src directives.
|
|
5
10
|
#
|
|
@@ -29,8 +34,67 @@ module SecureHeaders
|
|
|
29
34
|
end
|
|
30
35
|
end
|
|
31
36
|
|
|
37
|
+
##
|
|
38
|
+
# Checks to see if the hashed code is expected and adds the hash source
|
|
39
|
+
# value to the current CSP.
|
|
40
|
+
#
|
|
41
|
+
# By default, in development/test/etc. an exception will be raised.
|
|
42
|
+
def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block)
|
|
43
|
+
hashed_tag(
|
|
44
|
+
:script,
|
|
45
|
+
:script_src,
|
|
46
|
+
Configuration.instance_variable_get(:@script_hashes),
|
|
47
|
+
raise_error_on_unrecognized_hash,
|
|
48
|
+
block
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def hashed_style_tag(raise_error_on_unrecognized_hash = nil, &block)
|
|
53
|
+
hashed_tag(
|
|
54
|
+
:style,
|
|
55
|
+
:style_src,
|
|
56
|
+
Configuration.instance_variable_get(:@style_hashes),
|
|
57
|
+
raise_error_on_unrecognized_hash,
|
|
58
|
+
block
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
32
62
|
private
|
|
33
63
|
|
|
64
|
+
def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block)
|
|
65
|
+
if raise_error_on_unrecognized_hash.nil?
|
|
66
|
+
raise_error_on_unrecognized_hash = ENV["RAILS_ENV"] != "production"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
content = capture(&block)
|
|
70
|
+
file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb')
|
|
71
|
+
|
|
72
|
+
if raise_error_on_unrecognized_hash
|
|
73
|
+
hash_value = hash_source(content)
|
|
74
|
+
message = unexpected_hash_error_message(file_path, content, hash_value)
|
|
75
|
+
|
|
76
|
+
if hashes.nil? || hashes[file_path].nil? || !hashes[file_path].include?(hash_value)
|
|
77
|
+
raise UnexpectedHashedScriptException.new(message)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
SecureHeaders.append_content_security_policy_directives(request, directive => hashes[file_path])
|
|
82
|
+
|
|
83
|
+
content_tag type, content
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def unexpected_hash_error_message(file_path, content, hash_value)
|
|
87
|
+
<<-EOF
|
|
88
|
+
\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
|
|
89
|
+
#{content}
|
|
90
|
+
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
|
|
91
|
+
#{file_path}:
|
|
92
|
+
- #{hash_value}\n\n
|
|
93
|
+
NOTE: dynamic javascript is not supported using script hash integration
|
|
94
|
+
on purpose. It defeats the point of using it in the first place.
|
|
95
|
+
EOF
|
|
96
|
+
end
|
|
97
|
+
|
|
34
98
|
def nonced_tag(type, content_or_options, block)
|
|
35
99
|
options = {}
|
|
36
100
|
content = if block
|
data/lib/secure_headers.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require "secure_headers/configuration"
|
|
2
|
+
require "secure_headers/hash_helper"
|
|
3
|
+
require "secure_headers/headers/cookie"
|
|
2
4
|
require "secure_headers/headers/public_key_pins"
|
|
3
5
|
require "secure_headers/headers/content_security_policy"
|
|
4
6
|
require "secure_headers/headers/x_frame_options"
|
|
@@ -7,6 +9,7 @@ require "secure_headers/headers/x_xss_protection"
|
|
|
7
9
|
require "secure_headers/headers/x_content_type_options"
|
|
8
10
|
require "secure_headers/headers/x_download_options"
|
|
9
11
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
12
|
+
require "secure_headers/headers/referrer_policy"
|
|
10
13
|
require "secure_headers/middleware"
|
|
11
14
|
require "secure_headers/railtie"
|
|
12
15
|
require "secure_headers/view_helper"
|
|
@@ -25,6 +28,7 @@ module SecureHeaders
|
|
|
25
28
|
ContentSecurityPolicy,
|
|
26
29
|
StrictTransportSecurity,
|
|
27
30
|
PublicKeyPins,
|
|
31
|
+
ReferrerPolicy,
|
|
28
32
|
XContentTypeOptions,
|
|
29
33
|
XDownloadOptions,
|
|
30
34
|
XFrameOptions,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
|
|
2
|
+
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx
|
|
3
|
+
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
4
|
+
INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
5
|
+
|
|
6
|
+
namespace :secure_headers do
|
|
7
|
+
include SecureHeaders::HashHelper
|
|
8
|
+
|
|
9
|
+
def is_erb?(filename)
|
|
10
|
+
filename =~ /\.erb\Z/
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def is_mustache?(filename)
|
|
14
|
+
filename =~ /\.mustache\Z/
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dynamic_content?(filename, inline_script)
|
|
18
|
+
(is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) ||
|
|
19
|
+
(is_erb?(filename) && inline_script =~ /<%.*%>/)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_inline_content(filename, regex, hashes)
|
|
23
|
+
file = File.read(filename)
|
|
24
|
+
file.scan(regex) do # TODO don't use gsub
|
|
25
|
+
inline_script = Regexp.last_match.captures.last
|
|
26
|
+
if dynamic_content?(filename, inline_script)
|
|
27
|
+
puts "Looks like there's some dynamic content inside of a tag :-/"
|
|
28
|
+
puts "That pretty much means the hash value will never match."
|
|
29
|
+
puts "Code: " + inline_script
|
|
30
|
+
puts "=" * 20
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
hashes << hash_source(inline_script)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def generate_inline_script_hashes(filename)
|
|
38
|
+
hashes = []
|
|
39
|
+
|
|
40
|
+
[INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex|
|
|
41
|
+
find_inline_content(filename, regex, hashes)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
hashes
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def generate_inline_style_hashes(filename)
|
|
48
|
+
hashes = []
|
|
49
|
+
|
|
50
|
+
[INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex|
|
|
51
|
+
find_inline_content(filename, regex, hashes)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
hashes
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
task :generate_hashes do |t, args|
|
|
58
|
+
script_hashes = {
|
|
59
|
+
"scripts" => {},
|
|
60
|
+
"styles" => {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename|
|
|
64
|
+
hashes = generate_inline_script_hashes(filename)
|
|
65
|
+
if hashes.any?
|
|
66
|
+
script_hashes["scripts"][filename] = hashes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
hashes = generate_inline_style_hashes(filename)
|
|
70
|
+
if hashes.any?
|
|
71
|
+
script_hashes["styles"][filename] = hashes
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file|
|
|
76
|
+
file.write(script_hashes.to_yaml)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
|
|
80
|
+
end
|
|
81
|
+
end
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |gem|
|
|
3
3
|
gem.name = "secure_headers"
|
|
4
|
-
gem.version = "3.
|
|
4
|
+
gem.version = "3.3.0"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
7
|
gem.description = 'Security related headers all in one gem.'
|
|
@@ -36,7 +36,7 @@ module SecureHeaders
|
|
|
36
36
|
|
|
37
37
|
config = Configuration.get(:test_override)
|
|
38
38
|
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
|
39
|
-
[:csp, :dynamic_csp, :
|
|
39
|
+
[:csp, :dynamic_csp, :cookies].each do |key|
|
|
40
40
|
expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -82,5 +82,13 @@ module SecureHeaders
|
|
|
82
82
|
override_config = Configuration.get(:second_override)
|
|
83
83
|
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
|
84
84
|
end
|
|
85
|
+
|
|
86
|
+
it "deprecates the secure_cookies configuration" do
|
|
87
|
+
expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/)
|
|
88
|
+
|
|
89
|
+
Configuration.default do |config|
|
|
90
|
+
config.secure_cookies = true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
85
93
|
end
|
|
86
94
|
end
|
|
@@ -104,6 +104,11 @@ module SecureHeaders
|
|
|
104
104
|
expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
it "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
|
|
108
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
109
|
+
expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'")
|
|
110
|
+
end
|
|
111
|
+
|
|
107
112
|
it "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
|
|
108
113
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
109
114
|
expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe Cookie do
|
|
5
|
+
let(:raw_cookie) { "_session=thisisatest" }
|
|
6
|
+
|
|
7
|
+
it "does not tamper with cookies when unconfigured" do
|
|
8
|
+
cookie = Cookie.new(raw_cookie, {})
|
|
9
|
+
expect(cookie.to_s).to eq(raw_cookie)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "preserves existing attributes" do
|
|
13
|
+
cookie = Cookie.new("_session=thisisatest; secure", secure: true)
|
|
14
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "prevents duplicate flagging of attributes" do
|
|
18
|
+
cookie = Cookie.new("_session=thisisatest; secure", secure: true)
|
|
19
|
+
expect(cookie.to_s.scan(/secure/i).count).to eq(1)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "Secure cookies" do
|
|
23
|
+
context "when configured with a boolean" do
|
|
24
|
+
it "flags cookies as Secure" do
|
|
25
|
+
cookie = Cookie.new(raw_cookie, secure: true)
|
|
26
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "when configured with a Hash" do
|
|
31
|
+
it "flags cookies as Secure when whitelisted" do
|
|
32
|
+
cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]})
|
|
33
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "does not flag cookies as Secure when excluded" do
|
|
37
|
+
cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] })
|
|
38
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "HttpOnly cookies" do
|
|
44
|
+
context "when configured with a boolean" do
|
|
45
|
+
it "flags cookies as HttpOnly" do
|
|
46
|
+
cookie = Cookie.new(raw_cookie, httponly: true)
|
|
47
|
+
expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context "when configured with a Hash" do
|
|
52
|
+
it "flags cookies as HttpOnly when whitelisted" do
|
|
53
|
+
cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]})
|
|
54
|
+
expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "does not flag cookies as HttpOnly when excluded" do
|
|
58
|
+
cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] })
|
|
59
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context "SameSite cookies" do
|
|
65
|
+
it "flags SameSite=Lax" do
|
|
66
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } })
|
|
67
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "flags SameSite=Lax when configured with a boolean" do
|
|
71
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: true})
|
|
72
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "does not flag cookies as SameSite=Lax when excluded" do
|
|
76
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } })
|
|
77
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "flags SameSite=Strict" do
|
|
81
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } })
|
|
82
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "does not flag cookies as SameSite=Strict when excluded" do
|
|
86
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } })
|
|
87
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "flags SameSite=Strict when configured with a boolean" do
|
|
91
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: true})
|
|
92
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "flags properly when both lax and strict are configured" do
|
|
96
|
+
raw_cookie = "_session=thisisatest"
|
|
97
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } })
|
|
98
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "ignores configuration if the cookie is already flagged" do
|
|
102
|
+
raw_cookie = "_session=thisisatest; SameSite=Strict"
|
|
103
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: true })
|
|
104
|
+
expect(cookie.to_s).to eq(raw_cookie)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context "with an invalid configuration" do
|
|
110
|
+
it "raises an exception when not configured with a Hash" do
|
|
111
|
+
expect do
|
|
112
|
+
Cookie.validate_config!("configuration")
|
|
113
|
+
end.to raise_error(CookiesConfigError)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "raises an exception when configured without a boolean/Hash" do
|
|
117
|
+
expect do
|
|
118
|
+
Cookie.validate_config!(secure: "true")
|
|
119
|
+
end.to raise_error(CookiesConfigError)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "raises an exception when both only and except filters are provided" do
|
|
123
|
+
expect do
|
|
124
|
+
Cookie.validate_config!(secure: { only: [], except: [] })
|
|
125
|
+
end.to raise_error(CookiesConfigError)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "raises an exception when SameSite is not configured with a Hash" do
|
|
129
|
+
expect do
|
|
130
|
+
Cookie.validate_config!(samesite: true)
|
|
131
|
+
end.to raise_error(CookiesConfigError)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
|
|
135
|
+
expect do
|
|
136
|
+
Cookie.validate_config!(samesite: { lax: true, strict: true})
|
|
137
|
+
end.to raise_error(CookiesConfigError)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
|
|
141
|
+
expect do
|
|
142
|
+
Cookie.validate_config!(samesite: { lax: true, strict: { only: ["_anything"] } })
|
|
143
|
+
end.to raise_error(CookiesConfigError)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "raises an exception when both only and except filters are provided to SameSite configurations" do
|
|
147
|
+
expect do
|
|
148
|
+
Cookie.validate_config!(samesite: { lax: { only: ["_anything"], except: ["_anythingelse"] } })
|
|
149
|
+
end.to raise_error(CookiesConfigError)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
|
|
153
|
+
expect do
|
|
154
|
+
Cookie.validate_config!(samesite: { lax: { only: ["_anything"] }, strict: { only: ["_anything"] } })
|
|
155
|
+
end.to raise_error(CookiesConfigError)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
|
|
159
|
+
expect do
|
|
160
|
+
Cookie.validate_config!(samesite: { lax: { except: ["_anything"] }, strict: { except: ["_anything"] } })
|
|
161
|
+
end.to raise_error(CookiesConfigError)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe ReferrerPolicy do
|
|
5
|
+
specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) }
|
|
6
|
+
specify { expect(ReferrerPolicy.make_header('no-referrer')).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) }
|
|
7
|
+
|
|
8
|
+
context "valid configuration values" do
|
|
9
|
+
it "accepts 'no-referrer'" do
|
|
10
|
+
expect do
|
|
11
|
+
ReferrerPolicy.validate_config!("no-referrer")
|
|
12
|
+
end.not_to raise_error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "accepts 'no-referrer-when-downgrade'" do
|
|
16
|
+
expect do
|
|
17
|
+
ReferrerPolicy.validate_config!("no-referrer-when-downgrade")
|
|
18
|
+
end.not_to raise_error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "accepts 'origin'" do
|
|
22
|
+
expect do
|
|
23
|
+
ReferrerPolicy.validate_config!("origin")
|
|
24
|
+
end.not_to raise_error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "accepts 'origin-when-cross-origin'" do
|
|
28
|
+
expect do
|
|
29
|
+
ReferrerPolicy.validate_config!("origin-when-cross-origin")
|
|
30
|
+
end.not_to raise_error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "accepts 'unsafe-url'" do
|
|
34
|
+
expect do
|
|
35
|
+
ReferrerPolicy.validate_config!("unsafe-url")
|
|
36
|
+
end.not_to raise_error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "accepts nil" do
|
|
40
|
+
expect do
|
|
41
|
+
ReferrerPolicy.validate_config!(nil)
|
|
42
|
+
end.not_to raise_error
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context 'invlaid configuration values' do
|
|
47
|
+
it "doesn't accept invalid values" do
|
|
48
|
+
expect do
|
|
49
|
+
ReferrerPolicy.validate_config!("open")
|
|
50
|
+
end.to raise_error(ReferrerPolicyConfigError)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|