secure_headers 3.6.0 → 3.7.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/.rspec +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +2 -0
- data/CHANGELOG.md +34 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/LICENSE +4 -199
- data/README.md +12 -3
- data/docs/hashes.md +1 -1
- data/lib/secure_headers/configuration.rb +25 -5
- data/lib/secure_headers/headers/clear_site_data.rb +12 -9
- data/lib/secure_headers/headers/content_security_policy.rb +2 -2
- data/lib/secure_headers/headers/content_security_policy_config.rb +45 -12
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +22 -22
- data/lib/secure_headers/headers/public_key_pins.rb +1 -1
- data/lib/secure_headers/headers/referrer_policy.rb +1 -1
- data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
- data/lib/secure_headers/headers/x_download_options.rb +1 -1
- data/lib/secure_headers/headers/x_frame_options.rb +1 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
- data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
- data/lib/secure_headers/view_helper.rb +2 -2
- data/lib/secure_headers.rb +5 -2
- data/secure_headers.gemspec +2 -2
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +9 -26
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +12 -8
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +1 -0
- data/spec/lib/secure_headers/view_helpers_spec.rb +0 -1
- data/spec/lib/secure_headers_spec.rb +31 -4
- data/spec/spec_helper.rb +1 -0
- metadata +9 -4
|
@@ -6,12 +6,7 @@ module SecureHeaders
|
|
|
6
6
|
base.attrs.each do |attr|
|
|
7
7
|
base.send(:define_method, "#{attr}=") do |value|
|
|
8
8
|
if self.class.attrs.include?(attr)
|
|
9
|
-
|
|
10
|
-
prev_value = self.instance_variable_get("@#{attr}")
|
|
11
|
-
self.instance_variable_set("@#{attr}", value)
|
|
12
|
-
if prev_value != self.instance_variable_get("@#{attr}")
|
|
13
|
-
@modified = true
|
|
14
|
-
end
|
|
9
|
+
write_attribute(attr, value)
|
|
15
10
|
else
|
|
16
11
|
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
|
17
12
|
end
|
|
@@ -20,6 +15,30 @@ module SecureHeaders
|
|
|
20
15
|
end
|
|
21
16
|
|
|
22
17
|
def initialize(hash)
|
|
18
|
+
@base_uri = nil
|
|
19
|
+
@block_all_mixed_content = nil
|
|
20
|
+
@child_src = nil
|
|
21
|
+
@connect_src = nil
|
|
22
|
+
@default_src = nil
|
|
23
|
+
@font_src = nil
|
|
24
|
+
@form_action = nil
|
|
25
|
+
@frame_ancestors = nil
|
|
26
|
+
@frame_src = nil
|
|
27
|
+
@img_src = nil
|
|
28
|
+
@manifest_src = nil
|
|
29
|
+
@media_src = nil
|
|
30
|
+
@object_src = nil
|
|
31
|
+
@plugin_types = nil
|
|
32
|
+
@preserve_schemes = nil
|
|
33
|
+
@report_only = nil
|
|
34
|
+
@report_uri = nil
|
|
35
|
+
@sandbox = nil
|
|
36
|
+
@script_nonce = nil
|
|
37
|
+
@script_src = nil
|
|
38
|
+
@style_nonce = nil
|
|
39
|
+
@style_src = nil
|
|
40
|
+
@upgrade_insecure_requests = nil
|
|
41
|
+
|
|
23
42
|
from_hash(hash)
|
|
24
43
|
@modified = false
|
|
25
44
|
end
|
|
@@ -52,8 +71,9 @@ module SecureHeaders
|
|
|
52
71
|
|
|
53
72
|
def to_h
|
|
54
73
|
self.class.attrs.each_with_object({}) do |key, hash|
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
value = self.send(key)
|
|
75
|
+
hash[key] = value unless value.nil?
|
|
76
|
+
end
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
def dup
|
|
@@ -73,14 +93,26 @@ module SecureHeaders
|
|
|
73
93
|
|
|
74
94
|
private
|
|
75
95
|
def from_hash(hash)
|
|
76
|
-
hash.
|
|
96
|
+
hash.each_pair do |k, v|
|
|
97
|
+
next if v.nil?
|
|
98
|
+
|
|
77
99
|
if self.class.attrs.include?(k)
|
|
78
|
-
|
|
100
|
+
write_attribute(k, v)
|
|
79
101
|
else
|
|
80
|
-
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{
|
|
102
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
|
|
81
103
|
end
|
|
82
104
|
end
|
|
83
105
|
end
|
|
106
|
+
|
|
107
|
+
def write_attribute(attr, value)
|
|
108
|
+
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
109
|
+
attr_variable = "@#{attr}"
|
|
110
|
+
prev_value = self.instance_variable_get(attr_variable)
|
|
111
|
+
self.instance_variable_set(attr_variable, value)
|
|
112
|
+
if prev_value != value
|
|
113
|
+
@modified = true
|
|
114
|
+
end
|
|
115
|
+
end
|
|
84
116
|
end
|
|
85
117
|
|
|
86
118
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
@@ -88,8 +120,9 @@ module SecureHeaders
|
|
|
88
120
|
CONFIG_KEY = :csp
|
|
89
121
|
HEADER_NAME = "Content-Security-Policy".freeze
|
|
90
122
|
|
|
123
|
+
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
91
124
|
def self.attrs
|
|
92
|
-
|
|
125
|
+
ATTRS
|
|
93
126
|
end
|
|
94
127
|
|
|
95
128
|
include DynamicConfig
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module SecureHeaders
|
|
3
|
+
class ExpectCertificateTransparencyConfigError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class ExpectCertificateTransparency
|
|
6
|
+
HEADER_NAME = "Expect-CT".freeze
|
|
7
|
+
CONFIG_KEY = :expect_certificate_transparency
|
|
8
|
+
INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
|
|
9
|
+
INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
|
|
10
|
+
REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
|
|
11
|
+
INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Public: Generate a Expect-CT header.
|
|
15
|
+
#
|
|
16
|
+
# Returns nil if not configured, returns header name and value if
|
|
17
|
+
# configured.
|
|
18
|
+
def make_header(config)
|
|
19
|
+
return if config.nil?
|
|
20
|
+
|
|
21
|
+
header = new(config)
|
|
22
|
+
[HEADER_NAME, header.value]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate_config!(config)
|
|
26
|
+
return if config.nil? || config == OPT_OUT
|
|
27
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
|
|
28
|
+
|
|
29
|
+
unless [true, false, nil].include?(config[:enforce])
|
|
30
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if !config[:max_age]
|
|
34
|
+
raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
|
|
35
|
+
elsif config[:max_age].to_s !~ /\A\d+\z/
|
|
36
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(config)
|
|
42
|
+
@enforced = config.fetch(:enforce, nil)
|
|
43
|
+
@max_age = config.fetch(:max_age, nil)
|
|
44
|
+
@report_uri = config.fetch(:report_uri, nil)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def value
|
|
48
|
+
header_value = [
|
|
49
|
+
enforced_directive,
|
|
50
|
+
max_age_directive,
|
|
51
|
+
report_uri_directive
|
|
52
|
+
].compact.join("; ").strip
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def enforced_directive
|
|
56
|
+
# Unfortunately `if @enforced` isn't enough here in case someone
|
|
57
|
+
# passes in a random string so let's be specific with it to prevent
|
|
58
|
+
# accidental enforcement.
|
|
59
|
+
"enforce" if @enforced == true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def max_age_directive
|
|
63
|
+
"max-age=#{@max_age}" if @max_age
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def report_uri_directive
|
|
67
|
+
"report-uri=\"#{@report_uri}\"" if @report_uri
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -62,25 +62,19 @@ module SecureHeaders
|
|
|
62
62
|
|
|
63
63
|
# All the directives currently under consideration for CSP level 3.
|
|
64
64
|
# https://w3c.github.io/webappsec/specs/CSP2/
|
|
65
|
+
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
65
66
|
MANIFEST_SRC = :manifest_src
|
|
66
|
-
|
|
67
|
+
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
|
67
68
|
DIRECTIVES_3_0 = [
|
|
68
69
|
DIRECTIVES_2_0,
|
|
69
|
-
MANIFEST_SRC,
|
|
70
|
-
REFLECTED_XSS
|
|
71
|
-
].flatten.freeze
|
|
72
|
-
|
|
73
|
-
# All the directives that are not currently in a formal spec, but have
|
|
74
|
-
# been implemented somewhere.
|
|
75
|
-
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
76
|
-
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
|
77
|
-
DIRECTIVES_DRAFT = [
|
|
78
70
|
BLOCK_ALL_MIXED_CONTENT,
|
|
71
|
+
MANIFEST_SRC,
|
|
79
72
|
UPGRADE_INSECURE_REQUESTS
|
|
80
|
-
].freeze
|
|
73
|
+
].flatten.freeze
|
|
81
74
|
|
|
82
75
|
EDGE_DIRECTIVES = DIRECTIVES_1_0
|
|
83
76
|
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
77
|
+
SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
|
|
84
78
|
|
|
85
79
|
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
86
80
|
BLOCK_ALL_MIXED_CONTENT,
|
|
@@ -98,18 +92,18 @@ module SecureHeaders
|
|
|
98
92
|
].freeze
|
|
99
93
|
|
|
100
94
|
FIREFOX_DIRECTIVES = (
|
|
101
|
-
|
|
95
|
+
DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
102
96
|
).freeze
|
|
103
97
|
|
|
104
98
|
FIREFOX_46_DIRECTIVES = (
|
|
105
|
-
|
|
99
|
+
DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
|
|
106
100
|
).freeze
|
|
107
101
|
|
|
108
102
|
CHROME_DIRECTIVES = (
|
|
109
|
-
|
|
103
|
+
DIRECTIVES_3_0
|
|
110
104
|
).freeze
|
|
111
105
|
|
|
112
|
-
ALL_DIRECTIVES =
|
|
106
|
+
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
|
|
113
107
|
|
|
114
108
|
# Think of default-src and report-uri as the beginning and end respectively,
|
|
115
109
|
# everything else is in between.
|
|
@@ -133,6 +127,7 @@ module SecureHeaders
|
|
|
133
127
|
"Firefox" => FIREFOX_DIRECTIVES,
|
|
134
128
|
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
|
|
135
129
|
"Safari" => SAFARI_DIRECTIVES,
|
|
130
|
+
"SafariTransitional" => SAFARI_10_DIRECTIVES,
|
|
136
131
|
"Edge" => EDGE_DIRECTIVES,
|
|
137
132
|
"Other" => CHROME_DIRECTIVES
|
|
138
133
|
}.freeze
|
|
@@ -154,7 +149,6 @@ module SecureHeaders
|
|
|
154
149
|
MEDIA_SRC => :source_list,
|
|
155
150
|
OBJECT_SRC => :source_list,
|
|
156
151
|
PLUGIN_TYPES => :source_list,
|
|
157
|
-
REFLECTED_XSS => :string,
|
|
158
152
|
REPORT_URI => :source_list,
|
|
159
153
|
SANDBOX => :source_list,
|
|
160
154
|
SCRIPT_SRC => :source_list,
|
|
@@ -271,20 +265,26 @@ module SecureHeaders
|
|
|
271
265
|
# copy the default-src value to the original config. This modifies the original hash.
|
|
272
266
|
def populate_fetch_source_with_default!(original, additions)
|
|
273
267
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
274
|
-
additions.
|
|
268
|
+
additions.each_key do |directive|
|
|
275
269
|
if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
|
|
276
270
|
if nonce_added?(original, additions)
|
|
277
271
|
inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
278
272
|
unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
|
|
279
|
-
original[inferred_directive] = original
|
|
273
|
+
original[inferred_directive] = default_for(directive, original)
|
|
280
274
|
end
|
|
281
275
|
else
|
|
282
|
-
original[directive] = original
|
|
276
|
+
original[directive] = default_for(directive, original)
|
|
283
277
|
end
|
|
284
278
|
end
|
|
285
279
|
end
|
|
286
280
|
end
|
|
287
281
|
|
|
282
|
+
def default_for(directive, original)
|
|
283
|
+
return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
|
|
284
|
+
return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
|
|
285
|
+
original[DEFAULT_SRC]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
288
|
def nonce_added?(original, additions)
|
|
289
289
|
[:script_nonce, :style_nonce].each do |nonce|
|
|
290
290
|
if additions[nonce] && !original[nonce]
|
|
@@ -340,9 +340,9 @@ module SecureHeaders
|
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
def ensure_valid_sources!(directive, source_expression)
|
|
343
|
-
source_expression.each do |
|
|
344
|
-
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(
|
|
345
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{
|
|
343
|
+
source_expression.each do |expression|
|
|
344
|
+
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
|
|
345
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
|
|
346
346
|
end
|
|
347
347
|
end
|
|
348
348
|
end
|
|
@@ -2,7 +2,7 @@ module SecureHeaders
|
|
|
2
2
|
class STSConfigError < StandardError; end
|
|
3
3
|
|
|
4
4
|
class StrictTransportSecurity
|
|
5
|
-
HEADER_NAME = 'Strict-Transport-Security'
|
|
5
|
+
HEADER_NAME = 'Strict-Transport-Security'.freeze
|
|
6
6
|
HSTS_MAX_AGE = "631138519"
|
|
7
7
|
DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
|
|
8
8
|
VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
|
|
@@ -2,7 +2,7 @@ module SecureHeaders
|
|
|
2
2
|
class XContentTypeOptionsConfigError < StandardError; end
|
|
3
3
|
|
|
4
4
|
class XContentTypeOptions
|
|
5
|
-
HEADER_NAME = "X-Content-Type-Options"
|
|
5
|
+
HEADER_NAME = "X-Content-Type-Options".freeze
|
|
6
6
|
DEFAULT_VALUE = "nosniff"
|
|
7
7
|
CONFIG_KEY = :x_content_type_options
|
|
8
8
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class XPCDPConfigError < StandardError; end
|
|
3
3
|
class XPermittedCrossDomainPolicies
|
|
4
|
-
HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
|
|
4
|
+
HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
|
|
5
5
|
DEFAULT_VALUE = 'none'
|
|
6
6
|
VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
|
|
7
7
|
CONFIG_KEY = :x_permitted_cross_domain_policies
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class XXssProtectionConfigError < StandardError; end
|
|
3
3
|
class XXssProtection
|
|
4
|
-
HEADER_NAME = 'X-XSS-Protection'
|
|
4
|
+
HEADER_NAME = 'X-XSS-Protection'.freeze
|
|
5
5
|
DEFAULT_VALUE = "1; mode=block"
|
|
6
6
|
VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
|
|
7
7
|
CONFIG_KEY = :x_xss_protection
|
|
@@ -95,9 +95,9 @@ module SecureHeaders
|
|
|
95
95
|
<<-EOF
|
|
96
96
|
\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
|
|
97
97
|
#{content}
|
|
98
|
-
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/
|
|
98
|
+
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
|
|
99
99
|
#{file_path}:
|
|
100
|
-
- #{hash_value}\n\n
|
|
100
|
+
- \"#{hash_value}\"\n\n
|
|
101
101
|
NOTE: dynamic javascript is not supported using script hash integration
|
|
102
102
|
on purpose. It defeats the point of using it in the first place.
|
|
103
103
|
EOF
|
data/lib/secure_headers.rb
CHANGED
|
@@ -11,6 +11,7 @@ require "secure_headers/headers/x_download_options"
|
|
|
11
11
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
12
12
|
require "secure_headers/headers/referrer_policy"
|
|
13
13
|
require "secure_headers/headers/clear_site_data"
|
|
14
|
+
require "secure_headers/headers/expect_certificate_transparency"
|
|
14
15
|
require "secure_headers/middleware"
|
|
15
16
|
require "secure_headers/railtie"
|
|
16
17
|
require "secure_headers/view_helper"
|
|
@@ -51,6 +52,7 @@ module SecureHeaders
|
|
|
51
52
|
CSP = ContentSecurityPolicy
|
|
52
53
|
|
|
53
54
|
ALL_HEADER_CLASSES = [
|
|
55
|
+
ExpectCertificateTransparency,
|
|
54
56
|
ClearSiteData,
|
|
55
57
|
ContentSecurityPolicyConfig,
|
|
56
58
|
ContentSecurityPolicyReportOnlyConfig,
|
|
@@ -163,7 +165,8 @@ module SecureHeaders
|
|
|
163
165
|
# returned is meant to be merged into the header value from `@app.call(env)`
|
|
164
166
|
# in Rack middleware.
|
|
165
167
|
def header_hash_for(request)
|
|
166
|
-
|
|
168
|
+
prevent_dup = true
|
|
169
|
+
config = config_for(request, prevent_dup)
|
|
167
170
|
headers = config.cached_headers
|
|
168
171
|
user_agent = UserAgent.parse(request.user_agent)
|
|
169
172
|
|
|
@@ -177,7 +180,7 @@ module SecureHeaders
|
|
|
177
180
|
|
|
178
181
|
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
179
182
|
if header = headers[klass::CONFIG_KEY]
|
|
180
|
-
header_name, value = if
|
|
183
|
+
header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
|
|
181
184
|
csp_header_for_ua(header, user_agent)
|
|
182
185
|
else
|
|
183
186
|
header
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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.7.0"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
|
-
gem.description = '
|
|
7
|
+
gem.description = 'Manages application of security headers with many safe defaults.'
|
|
8
8
|
gem.summary = 'Add easily configured security headers to responses
|
|
9
9
|
including content-security-policy, x-frame-options,
|
|
10
10
|
strict-transport-security, etc.'
|
|
@@ -19,30 +19,16 @@ module SecureHeaders
|
|
|
19
19
|
name, value = described_class.make_header(true)
|
|
20
20
|
|
|
21
21
|
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
22
|
-
expect(value).to eq(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"cache",
|
|
26
|
-
"cookies",
|
|
27
|
-
"storage",
|
|
28
|
-
"executionContexts"
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
HERE
|
|
22
|
+
expect(value).to eq(
|
|
23
|
+
%("cache", "cookies", "storage", "executionContexts")
|
|
24
|
+
)
|
|
32
25
|
end
|
|
33
26
|
|
|
34
27
|
it "returns specified types" do
|
|
35
28
|
name, value = described_class.make_header(["foo", "bar"])
|
|
36
29
|
|
|
37
30
|
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
38
|
-
expect(value).to eq(
|
|
39
|
-
{
|
|
40
|
-
"types": [
|
|
41
|
-
"foo",
|
|
42
|
-
"bar"
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
HERE
|
|
31
|
+
expect(value).to eq(%("foo", "bar"))
|
|
46
32
|
end
|
|
47
33
|
end
|
|
48
34
|
|
|
@@ -83,12 +69,6 @@ module SecureHeaders
|
|
|
83
69
|
end.to raise_error(ClearSiteDataConfigError)
|
|
84
70
|
end
|
|
85
71
|
|
|
86
|
-
it "fails for non-serializable config" do
|
|
87
|
-
expect do
|
|
88
|
-
described_class.validate_config!(["hi \255"])
|
|
89
|
-
end.to raise_error(ClearSiteDataConfigError)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
72
|
it "fails for other types of config" do
|
|
93
73
|
expect do
|
|
94
74
|
described_class.validate_config!(:cookies)
|
|
@@ -96,8 +76,11 @@ module SecureHeaders
|
|
|
96
76
|
end
|
|
97
77
|
end
|
|
98
78
|
|
|
99
|
-
|
|
100
|
-
|
|
79
|
+
describe "make_header_value" do
|
|
80
|
+
it "returns a string of quoted values that are comma separated" do
|
|
81
|
+
value = described_class.make_header_value(["foo", "bar"])
|
|
82
|
+
expect(value).to eq(%("foo", "bar"))
|
|
83
|
+
end
|
|
101
84
|
end
|
|
102
85
|
end
|
|
103
86
|
end
|
|
@@ -86,8 +86,8 @@ module SecureHeaders
|
|
|
86
86
|
expect(csp.value).to eq("default-src example.org")
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
it "
|
|
90
|
-
expect(Kernel).
|
|
89
|
+
it "does not emit a warning when using frame-src" do
|
|
90
|
+
expect(Kernel).to_not receive(:warn)
|
|
91
91
|
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
92
92
|
end
|
|
93
93
|
|
|
@@ -119,7 +119,6 @@ module SecureHeaders
|
|
|
119
119
|
end.merge({
|
|
120
120
|
block_all_mixed_content: true,
|
|
121
121
|
upgrade_insecure_requests: true,
|
|
122
|
-
reflected_xss: "block",
|
|
123
122
|
script_src: %w(script-src.com),
|
|
124
123
|
script_nonce: 123456
|
|
125
124
|
})
|
|
@@ -127,22 +126,22 @@ module SecureHeaders
|
|
|
127
126
|
|
|
128
127
|
it "does not filter any directives for Chrome" do
|
|
129
128
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
130
|
-
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; 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")
|
|
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")
|
|
131
130
|
end
|
|
132
131
|
|
|
133
132
|
it "does not filter any directives for Opera" do
|
|
134
133
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
135
|
-
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; 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")
|
|
134
|
+
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")
|
|
136
135
|
end
|
|
137
136
|
|
|
138
137
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
139
138
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
140
|
-
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; 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")
|
|
139
|
+
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")
|
|
141
140
|
end
|
|
142
141
|
|
|
143
142
|
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
144
143
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
145
|
-
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; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
144
|
+
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")
|
|
146
145
|
end
|
|
147
146
|
|
|
148
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 Edge" do
|
|
@@ -155,11 +154,16 @@ module SecureHeaders
|
|
|
155
154
|
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")
|
|
156
155
|
end
|
|
157
156
|
|
|
157
|
+
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
|
|
158
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
|
|
159
|
+
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")
|
|
160
|
+
end
|
|
161
|
+
|
|
158
162
|
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
159
163
|
ua = USER_AGENTS[:firefox].dup
|
|
160
164
|
allow(ua).to receive(:version).and_return(nil)
|
|
161
165
|
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
162
|
-
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; 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")
|
|
166
|
+
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")
|
|
163
167
|
end
|
|
164
168
|
end
|
|
165
169
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
|
|
4
|
+
module SecureHeaders
|
|
5
|
+
describe ExpectCertificateTransparency do
|
|
6
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce; max-age=1234") }
|
|
7
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") }
|
|
8
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") }
|
|
9
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"") }
|
|
10
|
+
specify do
|
|
11
|
+
config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" }
|
|
12
|
+
header_value = "enforce; max-age=1234; report-uri=\"https://report-uri.io/expect-ct\""
|
|
13
|
+
expect(ExpectCertificateTransparency.new(config).value).to eq(header_value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context "with an invalid configuration" do
|
|
17
|
+
it "raises an exception when configuration isn't a hash" do
|
|
18
|
+
expect do
|
|
19
|
+
ExpectCertificateTransparency.validate_config!(%w(a))
|
|
20
|
+
end.to raise_error(ExpectCertificateTransparencyConfigError)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "raises an exception when max-age is not provided" do
|
|
24
|
+
expect do
|
|
25
|
+
ExpectCertificateTransparency.validate_config!(foo: "bar")
|
|
26
|
+
end.to raise_error(ExpectCertificateTransparencyConfigError)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "raises an exception with an invalid max-age" do
|
|
30
|
+
expect do
|
|
31
|
+
ExpectCertificateTransparency.validate_config!(max_age: "abc123")
|
|
32
|
+
end.to raise_error(ExpectCertificateTransparencyConfigError)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "raises an exception with an invalid enforce value" do
|
|
36
|
+
expect do
|
|
37
|
+
ExpectCertificateTransparency.validate_config!(enforce: "brokenstring")
|
|
38
|
+
end.to raise_error(ExpectCertificateTransparencyConfigError)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|