secure_headers 6.0.0.alpha01 → 6.0.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 +2 -2
- data/README.md +2 -2
- data/docs/upgrading-to-6-0.md +3 -3
- data/lib/secure_headers/configuration.rb +2 -2
- data/lib/secure_headers/headers/content_security_policy.rb +5 -58
- data/lib/secure_headers/headers/policy_management.rb +3 -55
- data/lib/secure_headers/view_helper.rb +9 -8
- data/lib/secure_headers.rb +1 -3
- data/secure_headers.gemspec +1 -2
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +1 -64
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +2 -2
- data/spec/lib/secure_headers/view_helpers_spec.rb +28 -0
- data/spec/lib/secure_headers_spec.rb +0 -37
- metadata +4 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d7392744d486b32bda2b91432d39ddfeee87dfd
|
|
4
|
+
data.tar.gz: 38328982bf71376b9412ed8c7d0652163741bf16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e08d9df89db6908d0c8419d0086bf8fb6c7e374c53e0bf774bd74d534094c66ef3cdcbac23e97c03b05d4045ba6878c3f6d8a17877146bdb6f2f0c15b57d1784
|
|
7
|
+
data.tar.gz: 304824374c236d11edc52049732a2ea133bbbdf611f35b2c5309a950125e879fb4016f0f15d7ad6c09310ab4b6080e966fac912147f43ed91989aa15898595a7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
## 6.0
|
|
2
2
|
|
|
3
3
|
- See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes.
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
## 5.0.5
|
|
6
6
|
|
|
7
|
-
A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
|
|
7
|
+
- A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
|
|
8
8
|
|
|
9
9
|
## 5.0.4
|
|
10
10
|
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Secure Headers [](http://travis-ci.org/twitter/secureheaders) [](https://codeclimate.com/github/twitter/secureheaders) [](https://coveralls.io/r/twitter/secureheaders)
|
|
2
2
|
|
|
3
|
-
**master represents
|
|
3
|
+
**master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now.
|
|
4
4
|
|
|
5
5
|
**The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](docs/upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
|
|
6
6
|
|
|
@@ -77,7 +77,7 @@ SecureHeaders::Configuration.default do |config|
|
|
|
77
77
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
78
78
|
|
|
79
79
|
# directive values: these values will directly translate into source directives
|
|
80
|
-
default_src: %w(
|
|
80
|
+
default_src: %w('none'),
|
|
81
81
|
base_uri: %w('self'),
|
|
82
82
|
block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/
|
|
83
83
|
child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
|
data/docs/upgrading-to-6-0.md
CHANGED
|
@@ -43,8 +43,8 @@ Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded
|
|
|
43
43
|
|
|
44
44
|
Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once.
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## All user agent sniffing has been removed
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
The policy configured is the policy that is delivered in terms of which directives are sent. We still dedup, strip schemes, and look for other optimizations but we will not e.g. conditionally send `frame-src` / `child-src` or apply `nonce`s / `unsafe-inline`.
|
|
49
49
|
|
|
50
|
-
The
|
|
50
|
+
The primary reason for these per-browser customization was to reduce console warnings. This has lead to many bugs and results inc confusing behavior. Also, console logs are incredibly noisy today and increasingly warn you about perfectly valid things (like sending `X-Frame-Options` and `frame-ancestors` together).
|
|
@@ -194,11 +194,11 @@ module SecureHeaders
|
|
|
194
194
|
self
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
-
def generate_headers
|
|
197
|
+
def generate_headers
|
|
198
198
|
headers = {}
|
|
199
199
|
HEADERABLE_ATTRIBUTES.each do |attr|
|
|
200
200
|
klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
|
|
201
|
-
header_name, value = klass.make_header(instance_variable_get("@#{attr}")
|
|
201
|
+
header_name, value = klass.make_header(instance_variable_get("@#{attr}"))
|
|
202
202
|
if header_name && value
|
|
203
203
|
headers[header_name] = value
|
|
204
204
|
end
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require_relative "policy_management"
|
|
3
3
|
require_relative "content_security_policy_config"
|
|
4
|
-
require "useragent"
|
|
5
4
|
|
|
6
5
|
module SecureHeaders
|
|
7
6
|
class ContentSecurityPolicy
|
|
8
7
|
include PolicyManagement
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
VERSION_46 = ::UserAgent::Version.new("46")
|
|
12
|
-
VERSION_10 = ::UserAgent::Version.new("10")
|
|
13
|
-
FALLBACK_VERSION = ::UserAgent::Version.new("0")
|
|
14
|
-
|
|
15
|
-
def initialize(config = nil, user_agent = OTHER)
|
|
16
|
-
user_agent ||= OTHER
|
|
9
|
+
def initialize(config = nil)
|
|
17
10
|
@config = if config.is_a?(Hash)
|
|
18
11
|
if config[:report_only]
|
|
19
12
|
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
|
@@ -26,12 +19,6 @@ module SecureHeaders
|
|
|
26
19
|
config
|
|
27
20
|
end
|
|
28
21
|
|
|
29
|
-
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
30
|
-
user_agent
|
|
31
|
-
else
|
|
32
|
-
UserAgent.parse(user_agent)
|
|
33
|
-
end
|
|
34
|
-
@frame_src = normalize_child_frame_src
|
|
35
22
|
@preserve_schemes = @config.preserve_schemes
|
|
36
23
|
@script_nonce = @config.script_nonce
|
|
37
24
|
@style_nonce = @config.style_nonce
|
|
@@ -56,20 +43,10 @@ module SecureHeaders
|
|
|
56
43
|
|
|
57
44
|
private
|
|
58
45
|
|
|
59
|
-
def normalize_child_frame_src
|
|
60
|
-
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
|
|
61
|
-
raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
@config.frame_src || @config.child_src
|
|
65
|
-
end
|
|
66
|
-
|
|
67
46
|
# Private: converts the config object into a string representing a policy.
|
|
68
47
|
# Places default-src at the first directive and report-uri as the last. All
|
|
69
48
|
# others are presented in alphabetical order.
|
|
70
49
|
#
|
|
71
|
-
# Unsupported directives are filtered based on the user agent.
|
|
72
|
-
#
|
|
73
50
|
# Returns a content security policy header value.
|
|
74
51
|
def build_value
|
|
75
52
|
directives.map do |directive_name|
|
|
@@ -125,18 +102,7 @@ module SecureHeaders
|
|
|
125
102
|
#
|
|
126
103
|
# Returns a string representing a directive.
|
|
127
104
|
def build_source_list_directive(directive)
|
|
128
|
-
source_list =
|
|
129
|
-
when :child_src
|
|
130
|
-
if supported_directives.include?(:child_src)
|
|
131
|
-
@frame_src
|
|
132
|
-
end
|
|
133
|
-
when :frame_src
|
|
134
|
-
unless supported_directives.include?(:child_src)
|
|
135
|
-
@frame_src
|
|
136
|
-
end
|
|
137
|
-
else
|
|
138
|
-
@config.directive_value(directive)
|
|
139
|
-
end
|
|
105
|
+
source_list = @config.directive_value(directive)
|
|
140
106
|
|
|
141
107
|
if source_list != OPT_OUT && source_list && source_list.any?
|
|
142
108
|
normalized_source_list = minify_source_list(directive, source_list)
|
|
@@ -219,13 +185,13 @@ module SecureHeaders
|
|
|
219
185
|
source_list
|
|
220
186
|
end
|
|
221
187
|
|
|
222
|
-
# Private: return the list of directives
|
|
188
|
+
# Private: return the list of directives,
|
|
223
189
|
# starting with default-src and ending with report-uri.
|
|
224
190
|
def directives
|
|
225
191
|
[
|
|
226
192
|
DEFAULT_SRC,
|
|
227
|
-
BODY_DIRECTIVES
|
|
228
|
-
REPORT_URI
|
|
193
|
+
BODY_DIRECTIVES,
|
|
194
|
+
REPORT_URI,
|
|
229
195
|
].flatten
|
|
230
196
|
end
|
|
231
197
|
|
|
@@ -234,25 +200,6 @@ module SecureHeaders
|
|
|
234
200
|
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
|
235
201
|
end
|
|
236
202
|
|
|
237
|
-
# Private: determine which directives are supported for the given user agent.
|
|
238
|
-
#
|
|
239
|
-
# Add UA-sniffing special casing here.
|
|
240
|
-
#
|
|
241
|
-
# Returns an array of symbols representing the directives.
|
|
242
|
-
def supported_directives
|
|
243
|
-
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
244
|
-
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
245
|
-
VARIATIONS["FirefoxTransitional"]
|
|
246
|
-
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
|
|
247
|
-
VARIATIONS["SafariTransitional"]
|
|
248
|
-
else
|
|
249
|
-
VARIATIONS[@parsed_ua.browser]
|
|
250
|
-
end
|
|
251
|
-
else
|
|
252
|
-
VARIATIONS[OTHER]
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
203
|
def symbol_to_hyphen_case(sym)
|
|
257
204
|
sym.to_s.tr("_", "-")
|
|
258
205
|
end
|
|
@@ -81,58 +81,12 @@ module SecureHeaders
|
|
|
81
81
|
UPGRADE_INSECURE_REQUESTS
|
|
82
82
|
].flatten.freeze
|
|
83
83
|
|
|
84
|
-
EDGE_DIRECTIVES = DIRECTIVES_1_0
|
|
85
|
-
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
86
|
-
SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
|
|
87
|
-
|
|
88
|
-
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
89
|
-
BLOCK_ALL_MIXED_CONTENT,
|
|
90
|
-
CHILD_SRC,
|
|
91
|
-
WORKER_SRC,
|
|
92
|
-
PLUGIN_TYPES
|
|
93
|
-
].freeze
|
|
94
|
-
|
|
95
|
-
FIREFOX_46_DEPRECATED_DIRECTIVES = [
|
|
96
|
-
FRAME_SRC
|
|
97
|
-
].freeze
|
|
98
|
-
|
|
99
|
-
FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
|
|
100
|
-
BLOCK_ALL_MIXED_CONTENT,
|
|
101
|
-
WORKER_SRC,
|
|
102
|
-
PLUGIN_TYPES
|
|
103
|
-
].freeze
|
|
104
|
-
|
|
105
|
-
FIREFOX_DIRECTIVES = (
|
|
106
|
-
DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
107
|
-
).freeze
|
|
108
|
-
|
|
109
|
-
FIREFOX_46_DIRECTIVES = (
|
|
110
|
-
DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
|
|
111
|
-
).freeze
|
|
112
|
-
|
|
113
|
-
CHROME_DIRECTIVES = (
|
|
114
|
-
DIRECTIVES_3_0
|
|
115
|
-
).freeze
|
|
116
|
-
|
|
117
84
|
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
|
|
118
85
|
|
|
119
86
|
# Think of default-src and report-uri as the beginning and end respectively,
|
|
120
87
|
# everything else is in between.
|
|
121
88
|
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
122
89
|
|
|
123
|
-
VARIATIONS = {
|
|
124
|
-
"Chrome" => CHROME_DIRECTIVES,
|
|
125
|
-
"Opera" => CHROME_DIRECTIVES,
|
|
126
|
-
"Firefox" => FIREFOX_DIRECTIVES,
|
|
127
|
-
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
|
|
128
|
-
"Safari" => SAFARI_DIRECTIVES,
|
|
129
|
-
"SafariTransitional" => SAFARI_10_DIRECTIVES,
|
|
130
|
-
"Edge" => EDGE_DIRECTIVES,
|
|
131
|
-
"Other" => CHROME_DIRECTIVES
|
|
132
|
-
}.freeze
|
|
133
|
-
|
|
134
|
-
OTHER = "Other".freeze
|
|
135
|
-
|
|
136
90
|
DIRECTIVE_VALUE_TYPES = {
|
|
137
91
|
BASE_URI => :source_list,
|
|
138
92
|
BLOCK_ALL_MIXED_CONTENT => :boolean,
|
|
@@ -199,9 +153,9 @@ module SecureHeaders
|
|
|
199
153
|
#
|
|
200
154
|
# Returns a default policy if no configuration is provided, or a
|
|
201
155
|
# header name and value based on the config.
|
|
202
|
-
def make_header(config
|
|
156
|
+
def make_header(config)
|
|
203
157
|
return if config.nil? || config == OPT_OUT
|
|
204
|
-
header = new(config
|
|
158
|
+
header = new(config)
|
|
205
159
|
[header.name, header.value]
|
|
206
160
|
end
|
|
207
161
|
|
|
@@ -303,17 +257,11 @@ module SecureHeaders
|
|
|
303
257
|
# Don't set a default if directive has an existing value
|
|
304
258
|
next if original[directive]
|
|
305
259
|
if FETCH_SOURCES.include?(directive)
|
|
306
|
-
original[directive] =
|
|
260
|
+
original[directive] = original[DEFAULT_SRC]
|
|
307
261
|
end
|
|
308
262
|
end
|
|
309
263
|
end
|
|
310
264
|
|
|
311
|
-
def default_for(directive, original)
|
|
312
|
-
return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
|
|
313
|
-
return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
|
|
314
|
-
original[DEFAULT_SRC]
|
|
315
|
-
end
|
|
316
|
-
|
|
317
265
|
def source_list?(directive)
|
|
318
266
|
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
|
319
267
|
end
|
|
@@ -19,7 +19,7 @@ module SecureHeaders
|
|
|
19
19
|
#
|
|
20
20
|
# Returns an html-safe link tag with the nonce attribute.
|
|
21
21
|
def nonced_stylesheet_link_tag(*args, &block)
|
|
22
|
-
opts = extract_options(args).merge(nonce:
|
|
22
|
+
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
|
|
23
23
|
|
|
24
24
|
stylesheet_link_tag(*args, opts, &block)
|
|
25
25
|
end
|
|
@@ -37,7 +37,7 @@ module SecureHeaders
|
|
|
37
37
|
#
|
|
38
38
|
# Returns an html-safe script tag with the nonce attribute.
|
|
39
39
|
def nonced_javascript_include_tag(*args, &block)
|
|
40
|
-
opts = extract_options(args).merge(nonce:
|
|
40
|
+
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
|
|
41
41
|
|
|
42
42
|
javascript_include_tag(*args, opts, &block)
|
|
43
43
|
end
|
|
@@ -47,7 +47,7 @@ module SecureHeaders
|
|
|
47
47
|
#
|
|
48
48
|
# Returns an html-safe script tag with the nonce attribute.
|
|
49
49
|
def nonced_javascript_pack_tag(*args, &block)
|
|
50
|
-
opts = extract_options(args).merge(nonce:
|
|
50
|
+
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
|
|
51
51
|
|
|
52
52
|
javascript_pack_tag(*args, opts, &block)
|
|
53
53
|
end
|
|
@@ -57,7 +57,7 @@ module SecureHeaders
|
|
|
57
57
|
#
|
|
58
58
|
# Returns an html-safe link tag with the nonce attribute.
|
|
59
59
|
def nonced_stylesheet_pack_tag(*args, &block)
|
|
60
|
-
opts = extract_options(args).merge(nonce:
|
|
60
|
+
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
|
|
61
61
|
|
|
62
62
|
stylesheet_pack_tag(*args, opts, &block)
|
|
63
63
|
end
|
|
@@ -66,7 +66,7 @@ module SecureHeaders
|
|
|
66
66
|
# Instructs secure_headers to append a nonce to style/script-src directives.
|
|
67
67
|
#
|
|
68
68
|
# Returns a non-html-safe nonce value.
|
|
69
|
-
def
|
|
69
|
+
def _content_security_policy_nonce(type)
|
|
70
70
|
case type
|
|
71
71
|
when :script
|
|
72
72
|
SecureHeaders.content_security_policy_script_nonce(@_request)
|
|
@@ -74,13 +74,14 @@ module SecureHeaders
|
|
|
74
74
|
SecureHeaders.content_security_policy_style_nonce(@_request)
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
|
+
alias_method :content_security_policy_nonce, :_content_security_policy_nonce
|
|
77
78
|
|
|
78
79
|
def content_security_policy_script_nonce
|
|
79
|
-
|
|
80
|
+
_content_security_policy_nonce(:script)
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
def content_security_policy_style_nonce
|
|
83
|
-
|
|
84
|
+
_content_security_policy_nonce(:style)
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
##
|
|
@@ -152,7 +153,7 @@ module SecureHeaders
|
|
|
152
153
|
else
|
|
153
154
|
content_or_options.html_safe # :'(
|
|
154
155
|
end
|
|
155
|
-
content_tag type, content, options.merge(nonce:
|
|
156
|
+
content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type))
|
|
156
157
|
end
|
|
157
158
|
|
|
158
159
|
def extract_options(args)
|
data/lib/secure_headers.rb
CHANGED
|
@@ -15,7 +15,6 @@ 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"
|
|
18
|
-
require "useragent"
|
|
19
18
|
require "singleton"
|
|
20
19
|
require "secure_headers/configuration"
|
|
21
20
|
|
|
@@ -149,8 +148,7 @@ module SecureHeaders
|
|
|
149
148
|
prevent_dup = true
|
|
150
149
|
config = config_for(request, prevent_dup)
|
|
151
150
|
config.validate_config!
|
|
152
|
-
|
|
153
|
-
headers = config.generate_headers(user_agent)
|
|
151
|
+
headers = config.generate_headers
|
|
154
152
|
|
|
155
153
|
if request.scheme != HTTPS
|
|
156
154
|
HTTPS_HEADER_CLASSES.each do |klass|
|
data/secure_headers.gemspec
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
Gem::Specification.new do |gem|
|
|
4
4
|
gem.name = "secure_headers"
|
|
5
|
-
gem.version = "6.0.0
|
|
5
|
+
gem.version = "6.0.0"
|
|
6
6
|
gem.authors = ["Neil Matatall"]
|
|
7
7
|
gem.email = ["neil.matatall@gmail.com"]
|
|
8
8
|
gem.description = "Manages application of security headers with many safe defaults."
|
|
@@ -16,5 +16,4 @@ Gem::Specification.new do |gem|
|
|
|
16
16
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
17
17
|
gem.require_paths = ["lib"]
|
|
18
18
|
gem.add_development_dependency "rake"
|
|
19
|
-
gem.add_dependency "useragent", ">= 0.15.0"
|
|
20
19
|
end
|
|
@@ -116,73 +116,10 @@ module SecureHeaders
|
|
|
116
116
|
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
it "raises an error when child-src and frame-src are supplied but are not equal" do
|
|
120
|
-
expect {
|
|
121
|
-
ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
|
|
122
|
-
}.to raise_error(ArgumentError)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
119
|
it "supports strict-dynamic" do
|
|
126
|
-
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}
|
|
120
|
+
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456})
|
|
127
121
|
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
|
|
128
122
|
end
|
|
129
|
-
|
|
130
|
-
context "browser sniffing" do
|
|
131
|
-
let (:complex_opts) do
|
|
132
|
-
(ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
|
|
133
|
-
hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
|
|
134
|
-
end.merge({
|
|
135
|
-
block_all_mixed_content: true,
|
|
136
|
-
upgrade_insecure_requests: true,
|
|
137
|
-
script_src: %w(script-src.com),
|
|
138
|
-
script_nonce: 123456,
|
|
139
|
-
sandbox: %w(allow-forms),
|
|
140
|
-
plugin_types: %w(application/pdf)
|
|
141
|
-
})
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
it "does not filter any directives for Chrome" do
|
|
145
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
146
|
-
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 application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it "does not filter any directives for Opera" do
|
|
150
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
151
|
-
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 application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
155
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
156
|
-
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 allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
160
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
161
|
-
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 allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
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, hash sources, and plugin-types for Edge" do
|
|
165
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
166
|
-
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 allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
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, hash sources, and plugin-types for safari" do
|
|
170
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
171
|
-
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 allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, and hash sources for safari 10 and higher" do
|
|
175
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
|
|
176
|
-
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 application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
180
|
-
ua = USER_AGENTS[:firefox].dup
|
|
181
|
-
allow(ua).to receive(:version).and_return(nil)
|
|
182
|
-
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
183
|
-
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 allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
123
|
end
|
|
187
124
|
end
|
|
188
125
|
end
|
|
@@ -190,7 +190,7 @@ module SecureHeaders
|
|
|
190
190
|
report_uri = "https://report-uri.io/asdf"
|
|
191
191
|
default_policy = Configuration.dup
|
|
192
192
|
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri])
|
|
193
|
-
csp = ContentSecurityPolicy.new(combined_config
|
|
193
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
|
194
194
|
expect(csp.value).to include("report-uri #{report_uri}")
|
|
195
195
|
end
|
|
196
196
|
|
|
@@ -223,7 +223,7 @@ module SecureHeaders
|
|
|
223
223
|
end
|
|
224
224
|
default_policy = Configuration.dup
|
|
225
225
|
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true)
|
|
226
|
-
csp = ContentSecurityPolicy.new(combined_config
|
|
226
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
|
227
227
|
expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
|
|
228
228
|
end
|
|
229
229
|
|
|
@@ -97,6 +97,12 @@ TEMPLATE
|
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
class MessageWithConflictingMethod < Message
|
|
101
|
+
def content_security_policy_nonce
|
|
102
|
+
"rails-nonce"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
100
106
|
module SecureHeaders
|
|
101
107
|
describe ViewHelpers do
|
|
102
108
|
let(:app) { lambda { |env| [200, env, "app"] } }
|
|
@@ -159,5 +165,27 @@ module SecureHeaders
|
|
|
159
165
|
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
|
|
160
166
|
end
|
|
161
167
|
end
|
|
168
|
+
|
|
169
|
+
it "avoids calling content_security_policy_nonce internally" do
|
|
170
|
+
begin
|
|
171
|
+
allow(SecureRandom).to receive(:base64).and_return("abc123")
|
|
172
|
+
|
|
173
|
+
expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8="
|
|
174
|
+
Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"])
|
|
175
|
+
expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc="
|
|
176
|
+
Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"])
|
|
177
|
+
|
|
178
|
+
# render erb that calls out to helpers.
|
|
179
|
+
MessageWithConflictingMethod.new(request).result
|
|
180
|
+
_, env = middleware.call request.env
|
|
181
|
+
|
|
182
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
|
|
183
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
|
|
184
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
|
|
185
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
|
|
186
|
+
|
|
187
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).not_to match(/rails-nonce/)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
162
190
|
end
|
|
163
191
|
end
|
|
@@ -127,27 +127,6 @@ module SecureHeaders
|
|
|
127
127
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
it "produces a UA-specific CSP when overriding (and busting the cache)" do
|
|
131
|
-
Configuration.default do |config|
|
|
132
|
-
config.csp = {
|
|
133
|
-
default_src: %w('self'),
|
|
134
|
-
script_src: %w('self'),
|
|
135
|
-
child_src: %w('self')
|
|
136
|
-
}
|
|
137
|
-
end
|
|
138
|
-
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
139
|
-
|
|
140
|
-
# append an unsupported directive
|
|
141
|
-
SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(application/pdf)})
|
|
142
|
-
# append a supported directive
|
|
143
|
-
SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
|
|
144
|
-
|
|
145
|
-
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
146
|
-
|
|
147
|
-
# child-src is translated to frame-src
|
|
148
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
|
|
149
|
-
end
|
|
150
|
-
|
|
151
130
|
it "produces a hash of headers with default config" do
|
|
152
131
|
Configuration.default
|
|
153
132
|
hash = SecureHeaders.header_hash_for(request)
|
|
@@ -197,22 +176,6 @@ module SecureHeaders
|
|
|
197
176
|
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
198
177
|
end
|
|
199
178
|
|
|
200
|
-
it "child-src and frame-src must match" do
|
|
201
|
-
Configuration.default do |config|
|
|
202
|
-
config.csp = {
|
|
203
|
-
default_src: %w('self'),
|
|
204
|
-
frame_src: %w(frame_src.com),
|
|
205
|
-
script_src: %w('self')
|
|
206
|
-
}
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
|
|
210
|
-
|
|
211
|
-
expect {
|
|
212
|
-
SecureHeaders.header_hash_for(chrome_request)
|
|
213
|
-
}.to raise_error(ArgumentError)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
179
|
it "supports named appends" do
|
|
217
180
|
Configuration.default do |config|
|
|
218
181
|
config.csp = {
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: secure_headers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.0.0
|
|
4
|
+
version: 6.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neil Matatall
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2018-
|
|
11
|
+
date: 2018-05-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -24,20 +24,6 @@ dependencies:
|
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: useragent
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.15.0
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.15.0
|
|
41
27
|
description: Manages application of security headers with many safe defaults.
|
|
42
28
|
email:
|
|
43
29
|
- neil.matatall@gmail.com
|
|
@@ -126,9 +112,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
126
112
|
version: '0'
|
|
127
113
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
114
|
requirements:
|
|
129
|
-
- - "
|
|
115
|
+
- - ">="
|
|
130
116
|
- !ruby/object:Gem::Version
|
|
131
|
-
version:
|
|
117
|
+
version: '0'
|
|
132
118
|
requirements: []
|
|
133
119
|
rubyforge_project:
|
|
134
120
|
rubygems_version: 2.6.13
|