secure_headers 3.3.0 → 3.4.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/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +2 -1
- data/README.md +18 -19
- data/lib/secure_headers/configuration.rb +2 -2
- data/lib/secure_headers/headers/content_security_policy.rb +34 -4
- data/lib/secure_headers/headers/policy_management.rb +14 -0
- data/lib/secure_headers/view_helper.rb +3 -5
- data/lib/tasks/tasks.rake +5 -4
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +47 -11
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +2 -2
- data/spec/lib/secure_headers_spec.rb +3 -2
- data/spec/spec_helper.rb +2 -1
- data/upgrading-to-3-0.md +13 -10
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88233251c7a26e1269f77957d56823619e3d9d11
|
|
4
|
+
data.tar.gz: 8e9c0e69fcca0298ee52223d587134f5a8df062f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f421d0d58eb9b5a7b22fd13932644c2c3868e0294886cbf5250127e8807fa20dec95717219e0a82ce59f49f376b32e17b97b77c7e011d2e2d1633162c1297bbc
|
|
7
|
+
data.tar.gz: ae63cbdd588bf6c31e531bfec5480e022a5df96c8ee2cc4c824d1e869f64df2a44073d795cc6d5e7ffeca6d6369f0d2e33f202ce8ceda04d274849e95789ff43
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Feature Requests
|
|
2
|
+
|
|
3
|
+
## Adding a new header
|
|
4
|
+
|
|
5
|
+
Generally, adding a new header is always OK.
|
|
6
|
+
|
|
7
|
+
* Is the header supported by any user agent? If so, which?
|
|
8
|
+
* What does it do?
|
|
9
|
+
* What are the valid values for the header?
|
|
10
|
+
* Where does the specification live?
|
|
11
|
+
|
|
12
|
+
## Adding a new CSP directive
|
|
13
|
+
|
|
14
|
+
* Is the directive supported by any user agent? If so, which?
|
|
15
|
+
* What does it do?
|
|
16
|
+
* What are the valid values for the directive?
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Bugs
|
|
21
|
+
|
|
22
|
+
Console errors and deprecation warnings are considered bugs that should be addressed with more precise UA sniffing. Bugs caused by incorrect or invalid UA sniffing are also bugs.
|
|
23
|
+
|
|
24
|
+
### Expected outcome
|
|
25
|
+
|
|
26
|
+
Describe what you expected to happen
|
|
27
|
+
|
|
28
|
+
1. I configure CSP to do X
|
|
29
|
+
1. When I inspect the response headers, the CSP should have included X
|
|
30
|
+
|
|
31
|
+
### Actual outcome
|
|
32
|
+
|
|
33
|
+
1. The generated policy did not include X
|
|
34
|
+
|
|
35
|
+
### Config
|
|
36
|
+
|
|
37
|
+
Please provide the configuration (`SecureHeaders::Configuration.default`) you are using including any overrides (`SecureHeaders::Configuration.override`).
|
|
38
|
+
|
|
39
|
+
### Generated headers
|
|
40
|
+
|
|
41
|
+
Provide a sample response containing the headers
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## All PRs:
|
|
2
|
+
|
|
3
|
+
* [ ] Has tests
|
|
4
|
+
* [ ] Documentation updated
|
|
5
|
+
|
|
6
|
+
## Adding a new header
|
|
7
|
+
|
|
8
|
+
Generally, adding a new header is always OK.
|
|
9
|
+
|
|
10
|
+
* Is the header supported by any user agent? If so, which?
|
|
11
|
+
* What does it do?
|
|
12
|
+
* What are the valid values for the header?
|
|
13
|
+
* Where does the specification live?
|
|
14
|
+
|
|
15
|
+
## Adding a new CSP directive
|
|
16
|
+
|
|
17
|
+
* Is the directive supported by any user agent? If so, which?
|
|
18
|
+
* What does it do?
|
|
19
|
+
* What are the valid values for the directive?
|
|
20
|
+
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 3.4.0 the frame-src/child-src transition for Firefox.
|
|
2
|
+
|
|
3
|
+
Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
if supported_directives.include?(:child_src)
|
|
7
|
+
@config[:child_src] = @config[:child_src] || @config[:frame_src]
|
|
8
|
+
else
|
|
9
|
+
@config[:frame_src] = @config[:frame_src] || @config[:child_src]
|
|
10
|
+
end
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Also, @koenpunt noticed that we were [loading view helpers](https://github.com/twitter/secureheaders/pull/272) in a way that Rails 5 did not like.
|
|
14
|
+
|
|
15
|
+
## 3.3.2 minor fix to silence warnings when using rake
|
|
16
|
+
|
|
17
|
+
[@dankohn](https://github.com/twitter/secureheaders/issues/257) was seeing "already initialized" errors in his output. This change conditionally defines the constants.
|
|
18
|
+
|
|
19
|
+
## 3.3.1 bugfix for boolean CSP directives
|
|
20
|
+
|
|
21
|
+
[@stefansundin](https://github.com/twitter/secureheaders/pull/253) noticed that supplying `false` to "boolean" CSP directives (e.g. `upgrade-insecure-requests` and `block-all-mixed-content`) would still include the value.
|
|
22
|
+
|
|
1
23
|
## 3.3.0 referrer-policy support
|
|
2
24
|
|
|
3
25
|
While not officially part of the spec and not implemented anywhere, support for the experimental [`referrer-policy` header](https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header) was [preemptively added](https://github.com/twitter/secureheaders/pull/249).
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
The gem will automatically apply several headers that are related to security. This includes:
|
|
9
9
|
- Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/)
|
|
10
10
|
- HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797)
|
|
11
|
-
- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options
|
|
11
|
+
- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034)
|
|
12
12
|
- X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx)
|
|
13
13
|
- X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx)
|
|
14
14
|
- X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx)
|
|
@@ -48,25 +48,24 @@ SecureHeaders::Configuration.default do |config|
|
|
|
48
48
|
config.referrer_policy = "origin-when-cross-origin"
|
|
49
49
|
config.csp = {
|
|
50
50
|
# "meta" values. these will shaped the header, but the values are not included in the header.
|
|
51
|
-
report_only:
|
|
51
|
+
report_only: true, # default: false
|
|
52
52
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
53
53
|
|
|
54
54
|
# directive values: these values will directly translate into source directives
|
|
55
55
|
default_src: %w(https: 'self'),
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
base_uri: %w('self'),
|
|
57
|
+
block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/
|
|
58
|
+
child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
|
|
59
|
+
connect_src: %w(wss:),
|
|
58
60
|
font_src: %w('self' data:),
|
|
61
|
+
form_action: %w('self' github.com),
|
|
62
|
+
frame_ancestors: %w('none'),
|
|
59
63
|
img_src: %w(mycdn.com data:),
|
|
60
64
|
media_src: %w(utoob.com),
|
|
61
65
|
object_src: %w('self'),
|
|
66
|
+
plugin_types: %w(application/x-shockwave-flash),
|
|
62
67
|
script_src: %w('self'),
|
|
63
68
|
style_src: %w('unsafe-inline'),
|
|
64
|
-
base_uri: %w('self'),
|
|
65
|
-
child_src: %w('self'),
|
|
66
|
-
form_action: %w('self' github.com),
|
|
67
|
-
frame_ancestors: %w('none'),
|
|
68
|
-
plugin_types: %w(application/x-shockwave-flash),
|
|
69
|
-
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
|
|
70
69
|
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
|
71
70
|
report_uri: %w(https://report-uri.io/example-csp)
|
|
72
71
|
}
|
|
@@ -85,7 +84,7 @@ end
|
|
|
85
84
|
|
|
86
85
|
### rails 2
|
|
87
86
|
|
|
88
|
-
For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 applications, an explicit statement is required to use the middleware component.
|
|
87
|
+
For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 or non-rails applications, an explicit statement is required to use the middleware component.
|
|
89
88
|
|
|
90
89
|
```ruby
|
|
91
90
|
use SecureHeaders::Middleware
|
|
@@ -137,7 +136,7 @@ class MyController < ApplicationController
|
|
|
137
136
|
end
|
|
138
137
|
```
|
|
139
138
|
|
|
140
|
-
By default, a
|
|
139
|
+
By default, a no-op configuration is provided. No headers will be set when this default override is used.
|
|
141
140
|
|
|
142
141
|
```ruby
|
|
143
142
|
class MyController < ApplicationController
|
|
@@ -163,12 +162,12 @@ You can override the settings for a given action by producing a temporary overri
|
|
|
163
162
|
class MyController < ApplicationController
|
|
164
163
|
def index
|
|
165
164
|
# Append value to the source list, override 'none' values
|
|
166
|
-
# Produces: default-src 'self'; script-src 'self' s3.
|
|
167
|
-
append_content_security_policy_directives(script_src: %w(s3.
|
|
165
|
+
# Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com
|
|
166
|
+
append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com))
|
|
168
167
|
|
|
169
168
|
# Overrides the previously set source list, override 'none' values
|
|
170
|
-
# Produces: default-src 'self'; script-src s3.
|
|
171
|
-
override_content_security_policy_directives(script_src: %w(s3.
|
|
169
|
+
# Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self'
|
|
170
|
+
override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self'))
|
|
172
171
|
|
|
173
172
|
# Global settings default to "sameorigin"
|
|
174
173
|
override_x_frame_options("DENY")
|
|
@@ -207,7 +206,7 @@ You can use a view helper to automatically add nonces to script tags:
|
|
|
207
206
|
|
|
208
207
|
```erb
|
|
209
208
|
<%= nonced_javascript_tag do %>
|
|
210
|
-
console.log("
|
|
209
|
+
console.log("nonced!");
|
|
211
210
|
<% end %>
|
|
212
211
|
|
|
213
212
|
<%= nonced_style_tag do %>
|
|
@@ -324,14 +323,14 @@ Be aware that pinning error reporting is governed by the same rules as everythin
|
|
|
324
323
|
|
|
325
324
|
```ruby
|
|
326
325
|
config.hpkp = {
|
|
327
|
-
max_age: 60.days.to_i,
|
|
326
|
+
max_age: 60.days.to_i, # max_age is a required parameter
|
|
328
327
|
include_subdomains: true, # whether or not to apply pins to subdomains
|
|
329
328
|
# Per the spec, SHA256 hashes are the only currently supported format.
|
|
330
329
|
pins: [
|
|
331
330
|
{sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'},
|
|
332
331
|
{sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'}
|
|
333
332
|
],
|
|
334
|
-
report_only: true,
|
|
333
|
+
report_only: true, # defaults to false (report-only mode)
|
|
335
334
|
report_uri: 'https://report-uri.io/example-hpkp'
|
|
336
335
|
}
|
|
337
336
|
```
|
|
@@ -203,7 +203,7 @@ module SecureHeaders
|
|
|
203
203
|
raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
-
@csp = new_csp
|
|
206
|
+
@csp = self.class.send(:deep_copy_if_hash, new_csp)
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
def cookies=(cookies)
|
|
@@ -215,7 +215,7 @@ module SecureHeaders
|
|
|
215
215
|
end
|
|
216
216
|
|
|
217
217
|
def hpkp=(hpkp)
|
|
218
|
-
@hpkp = hpkp
|
|
218
|
+
@hpkp = self.class.send(:deep_copy_if_hash, hpkp)
|
|
219
219
|
end
|
|
220
220
|
|
|
221
221
|
def hpkp_report_host=(hpkp_report_host)
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
require_relative 'policy_management'
|
|
2
|
+
require 'useragent'
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
5
6
|
class ContentSecurityPolicy
|
|
6
7
|
include PolicyManagement
|
|
7
8
|
|
|
9
|
+
# constants to be used for version-specific UA sniffing
|
|
10
|
+
VERSION_46 = ::UserAgent::Version.new("46")
|
|
11
|
+
|
|
8
12
|
def initialize(config = nil, user_agent = OTHER)
|
|
9
|
-
config = Configuration.deep_copy
|
|
10
|
-
@config = config
|
|
13
|
+
@config = Configuration.send(:deep_copy, config || DEFAULT_CONFIG)
|
|
11
14
|
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
12
15
|
user_agent
|
|
13
16
|
else
|
|
14
17
|
UserAgent.parse(user_agent)
|
|
15
18
|
end
|
|
19
|
+
normalize_child_frame_src
|
|
16
20
|
@report_only = @config[:report_only]
|
|
17
21
|
@preserve_schemes = @config[:preserve_schemes]
|
|
18
22
|
@script_nonce = @config[:script_nonce]
|
|
@@ -42,6 +46,22 @@ module SecureHeaders
|
|
|
42
46
|
|
|
43
47
|
private
|
|
44
48
|
|
|
49
|
+
# frame-src is deprecated, child-src is being implemented. They are
|
|
50
|
+
# very similar and in most cases, the same value can be used for both.
|
|
51
|
+
def normalize_child_frame_src
|
|
52
|
+
if @config[:frame_src] && @config[:child_src] && @config[:frame_src] != @config[:child_src]
|
|
53
|
+
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
|
|
54
|
+
elsif @config[:frame_src]
|
|
55
|
+
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}.")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if supported_directives.include?(:child_src)
|
|
59
|
+
@config[:child_src] = @config[:child_src] || @config[:frame_src]
|
|
60
|
+
else
|
|
61
|
+
@config[:frame_src] = @config[:frame_src] || @config[:child_src]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
45
65
|
# Private: converts the config object into a string representing a policy.
|
|
46
66
|
# Places default-src at the first directive and report-uri as the last. All
|
|
47
67
|
# others are presented in alphabetical order.
|
|
@@ -53,7 +73,7 @@ module SecureHeaders
|
|
|
53
73
|
directives.map do |directive_name|
|
|
54
74
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
55
75
|
when :boolean
|
|
56
|
-
symbol_to_hyphen_case(directive_name)
|
|
76
|
+
symbol_to_hyphen_case(directive_name) if @config[directive_name]
|
|
57
77
|
when :string
|
|
58
78
|
[symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
|
|
59
79
|
else
|
|
@@ -162,9 +182,19 @@ module SecureHeaders
|
|
|
162
182
|
|
|
163
183
|
# Private: determine which directives are supported for the given user agent.
|
|
164
184
|
#
|
|
185
|
+
# Add UA-sniffing special casing here.
|
|
186
|
+
#
|
|
165
187
|
# Returns an array of symbols representing the directives.
|
|
166
188
|
def supported_directives
|
|
167
|
-
@supported_directives ||= VARIATIONS[@parsed_ua.browser]
|
|
189
|
+
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
190
|
+
if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
|
|
191
|
+
VARIATIONS["FirefoxTransitional"]
|
|
192
|
+
else
|
|
193
|
+
VARIATIONS[@parsed_ua.browser]
|
|
194
|
+
end
|
|
195
|
+
else
|
|
196
|
+
VARIATIONS[OTHER]
|
|
197
|
+
end
|
|
168
198
|
end
|
|
169
199
|
|
|
170
200
|
def nonces_supported?
|
|
@@ -100,10 +100,23 @@ module SecureHeaders
|
|
|
100
100
|
PLUGIN_TYPES
|
|
101
101
|
].freeze
|
|
102
102
|
|
|
103
|
+
FIREFOX_46_DEPRECATED_DIRECTIVES = [
|
|
104
|
+
FRAME_SRC
|
|
105
|
+
].freeze
|
|
106
|
+
|
|
107
|
+
FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
|
|
108
|
+
BLOCK_ALL_MIXED_CONTENT,
|
|
109
|
+
PLUGIN_TYPES
|
|
110
|
+
].freeze
|
|
111
|
+
|
|
103
112
|
FIREFOX_DIRECTIVES = (
|
|
104
113
|
DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
105
114
|
).freeze
|
|
106
115
|
|
|
116
|
+
FIREFOX_46_DIRECTIVES = (
|
|
117
|
+
DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
|
|
118
|
+
).freeze
|
|
119
|
+
|
|
107
120
|
CHROME_DIRECTIVES = (
|
|
108
121
|
DIRECTIVES_2_0 + DIRECTIVES_DRAFT
|
|
109
122
|
).freeze
|
|
@@ -118,6 +131,7 @@ module SecureHeaders
|
|
|
118
131
|
"Chrome" => CHROME_DIRECTIVES,
|
|
119
132
|
"Opera" => CHROME_DIRECTIVES,
|
|
120
133
|
"Firefox" => FIREFOX_DIRECTIVES,
|
|
134
|
+
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
|
|
121
135
|
"Safari" => SAFARI_DIRECTIVES,
|
|
122
136
|
"Edge" => EDGE_DIRECTIVES,
|
|
123
137
|
"Other" => CHROME_DIRECTIVES
|
|
@@ -108,8 +108,6 @@ module SecureHeaders
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
end
|
|
115
|
-
end
|
|
111
|
+
ActiveSupport.on_load :action_view do
|
|
112
|
+
include SecureHeaders::ViewHelpers
|
|
113
|
+
end if defined?(ActiveSupport)
|
data/lib/tasks/tasks.rake
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
1
|
+
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
|
|
2
|
+
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
|
|
3
|
+
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
|
|
4
|
+
INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX
|
|
5
5
|
|
|
6
6
|
namespace :secure_headers do
|
|
7
7
|
include SecureHeaders::HashHelper
|
|
@@ -54,6 +54,7 @@ namespace :secure_headers do
|
|
|
54
54
|
hashes
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
|
|
57
58
|
task :generate_hashes do |t, args|
|
|
58
59
|
script_hashes = {
|
|
59
60
|
"scripts" => {},
|
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.4.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.'
|
|
@@ -24,7 +24,7 @@ module SecureHeaders
|
|
|
24
24
|
|
|
25
25
|
describe "#value" do
|
|
26
26
|
it "discards 'none' values if any other source expressions are present" do
|
|
27
|
-
csp = ContentSecurityPolicy.new(default_opts.merge(
|
|
27
|
+
csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
|
|
28
28
|
expect(csp.value).not_to include("'none'")
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -71,47 +71,83 @@ module SecureHeaders
|
|
|
71
71
|
expect(csp.value).to eq("default-src example.org")
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
it "does add a boolean directive if the value is true" do
|
|
75
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: true)
|
|
76
|
+
expect(csp.value).to eq("default-src example.org; block-all-mixed-content; upgrade-insecure-requests")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "does not add a boolean directive if the value is false" do
|
|
80
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: false)
|
|
81
|
+
expect(csp.value).to eq("default-src example.org; block-all-mixed-content")
|
|
82
|
+
end
|
|
83
|
+
|
|
74
84
|
it "deduplicates any source expressions" do
|
|
75
85
|
csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org))
|
|
76
86
|
expect(csp.value).to eq("default-src example.org")
|
|
77
87
|
end
|
|
78
88
|
|
|
89
|
+
it "emits a warning when using frame-src" do
|
|
90
|
+
expect(Kernel).to receive(:warn).with(/:frame_src is deprecated, use :child_src instead./)
|
|
91
|
+
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "emits a warning when child-src and frame-src are supplied but are not equal" do
|
|
95
|
+
expect(Kernel).to receive(:warn).with(/both :child_src and :frame_src supplied and do not match./)
|
|
96
|
+
ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
|
|
97
|
+
end
|
|
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/)
|
|
108
|
+
end
|
|
109
|
+
|
|
79
110
|
context "browser sniffing" do
|
|
80
111
|
let (:complex_opts) do
|
|
81
|
-
ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) do |directive, hash|
|
|
82
|
-
hash[directive] =
|
|
112
|
+
(ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
|
|
113
|
+
hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
|
|
83
114
|
end.merge({
|
|
84
115
|
block_all_mixed_content: true,
|
|
85
116
|
upgrade_insecure_requests: true,
|
|
86
117
|
reflected_xss: "block",
|
|
87
|
-
script_src: %w(
|
|
118
|
+
script_src: %w(script-src.com),
|
|
88
119
|
script_nonce: 123456
|
|
89
120
|
})
|
|
90
121
|
end
|
|
91
122
|
|
|
92
123
|
it "does not filter any directives for Chrome" do
|
|
93
124
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
94
|
-
expect(policy.value).to eq("default-src
|
|
125
|
+
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")
|
|
95
126
|
end
|
|
96
127
|
|
|
97
128
|
it "does not filter any directives for Opera" do
|
|
98
129
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
99
|
-
expect(policy.value).to eq("default-src
|
|
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")
|
|
100
131
|
end
|
|
101
132
|
|
|
102
133
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
103
134
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
104
|
-
expect(policy.value).to eq("default-src
|
|
135
|
+
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")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
139
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
140
|
+
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")
|
|
105
141
|
end
|
|
106
142
|
|
|
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
|
|
143
|
+
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
|
|
108
144
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
109
|
-
expect(policy.value).to eq("default-src
|
|
145
|
+
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")
|
|
110
146
|
end
|
|
111
147
|
|
|
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
|
|
148
|
+
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
|
|
113
149
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
114
|
-
expect(policy.value).to eq("default-src
|
|
150
|
+
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")
|
|
115
151
|
end
|
|
116
152
|
end
|
|
117
153
|
end
|
|
@@ -23,7 +23,8 @@ module SecureHeaders
|
|
|
23
23
|
# directive values: these values will directly translate into source directives
|
|
24
24
|
default_src: %w(https: 'self'),
|
|
25
25
|
frame_src: %w('self' *.twimg.com itunes.apple.com),
|
|
26
|
-
|
|
26
|
+
child_src: %w('self' *.twimg.com itunes.apple.com),
|
|
27
|
+
connect_src: %w(wss:),
|
|
27
28
|
font_src: %w('self' data:),
|
|
28
29
|
img_src: %w(mycdn.com data:),
|
|
29
30
|
media_src: %w(utoob.com),
|
|
@@ -31,7 +32,6 @@ module SecureHeaders
|
|
|
31
32
|
script_src: %w('self'),
|
|
32
33
|
style_src: %w('unsafe-inline'),
|
|
33
34
|
base_uri: %w('self'),
|
|
34
|
-
child_src: %w('self'),
|
|
35
35
|
form_action: %w('self' github.com),
|
|
36
36
|
frame_ancestors: %w('none'),
|
|
37
37
|
plugin_types: %w(application/x-shockwave-flash),
|
|
@@ -90,8 +90,7 @@ module SecureHeaders
|
|
|
90
90
|
config = Configuration.default do |config|
|
|
91
91
|
config.csp = {
|
|
92
92
|
default_src: %w('self'),
|
|
93
|
-
child_src: %w('self')
|
|
94
|
-
frame_src: %w('self')
|
|
93
|
+
child_src: %w('self')
|
|
95
94
|
}
|
|
96
95
|
end
|
|
97
96
|
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
@@ -102,6 +101,8 @@ module SecureHeaders
|
|
|
102
101
|
SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
|
|
103
102
|
|
|
104
103
|
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
104
|
+
|
|
105
|
+
# child-src is translated to frame-src
|
|
105
106
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
|
|
106
107
|
end
|
|
107
108
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -12,7 +12,8 @@ require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
|
|
|
12
12
|
|
|
13
13
|
USER_AGENTS = {
|
|
14
14
|
edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
|
|
15
|
-
firefox:
|
|
15
|
+
firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1",
|
|
16
|
+
firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0",
|
|
16
17
|
chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5',
|
|
17
18
|
ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
|
|
18
19
|
opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
|
data/upgrading-to-3-0.md
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
Changes
|
|
4
4
|
==
|
|
5
5
|
|
|
6
|
-
| What
|
|
7
|
-
|
|
8
|
-
| Global configuration
|
|
9
|
-
| All headers besides HPKP and CSP
|
|
10
|
-
| CSP directive values
|
|
11
|
-
| CSP Nonce values in views
|
|
12
|
-
|
|
|
13
|
-
| `
|
|
14
|
-
|
|
|
15
|
-
|
|
|
6
|
+
| What | < = 2.x | >= 3.0 |
|
|
7
|
+
| ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block |
|
|
9
|
+
| All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) |
|
|
10
|
+
| CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings |
|
|
11
|
+
| CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_nonce(:script)` or `content_security_policy_nonce(:style)` |
|
|
12
|
+
| nonce is no longer a source expression | `config.csp = "'self' 'nonce'"` | Remove `'nonce'` from source expression and use [nonce helpers](https://github.com/twitter/secureheaders#nonce). |
|
|
13
|
+
| `self`/`none` source expressions | Could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` |
|
|
14
|
+
| `inline` / `eval` source expressions | Could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` |
|
|
15
|
+
| Per-action configuration | Override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) |
|
|
16
|
+
| CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` |
|
|
17
|
+
| Schemes in source expressions | Schemes were not stripped | Schemes are stripped by default to discourage mixed content. Setting `preserve_schemes: true` will revert to previous behavior |
|
|
18
|
+
| Opting out of default configuration | `skip_before_filter :set_x_download_options_header` or `config.x_download_options = false` | Within default block: `config.x_download_options = SecureHeaders::OPT_OUT` |
|
|
16
19
|
|
|
17
20
|
Migrating to 3.x from <= 2.x
|
|
18
21
|
==
|
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: 3.
|
|
4
|
+
version: 3.4.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: 2016-
|
|
11
|
+
date: 2016-07-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -45,6 +45,8 @@ executables: []
|
|
|
45
45
|
extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
|
+
- ".github/ISSUE_TEMPLATE.md"
|
|
49
|
+
- ".github/PULL_REQUEST_TEMPLATE.md"
|
|
48
50
|
- ".gitignore"
|
|
49
51
|
- ".rspec"
|
|
50
52
|
- ".ruby-gemset"
|