secure_headers 6.0.0 → 6.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/.travis.yml +1 -2
- data/CHANGELOG.md +4 -0
- data/README.md +17 -18
- data/lib/secure_headers.rb +3 -11
- data/lib/secure_headers/configuration.rb +0 -9
- data/lib/secure_headers/headers/content_security_policy.rb +2 -2
- data/lib/secure_headers/headers/content_security_policy_config.rb +3 -0
- data/lib/secure_headers/headers/policy_management.rb +37 -5
- data/lib/secure_headers/middleware.rb +0 -6
- data/lib/secure_headers/utils/cookies_config.rb +1 -1
- data/lib/secure_headers/version.rb +3 -0
- data/secure_headers.gemspec +5 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +25 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +14 -9
- data/spec/lib/secure_headers/middleware_spec.rb +0 -19
- data/spec/lib/secure_headers_spec.rb +0 -35
- data/spec/spec_helper.rb +0 -1
- metadata +4 -7
- data/lib/secure_headers/headers/public_key_pins.rb +0 -81
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 26a6b47b87ec772f94c264ef318bf4c20997b3a541898f80110a1bd26a471753
|
4
|
+
data.tar.gz: 73e40f805bafe030a82cbe37cdf499acea6c2613cce3c25964db89a9fde2f172
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed3eb3ba011292b668fd786a22a2c9d93a20067e4391881eafc555f978a7ae3c5da33741f82c908b7cfe4c66cb837f9fc1b2f7360be788d320dab0694fdaeb66
|
7
|
+
data.tar.gz: df5800647d9dc5462d51529461d85374831bdc1fe0755949d4ec9764f69810c7e5456be1095f25d4b34bb2ec2c3c0773243d2ce4ebaf4c11bace6ea035201947
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.6.1
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -2,10 +2,6 @@
|
|
2
2
|
|
3
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
|
-
**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
|
-
|
7
|
-
**The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be not be maintained once 4.x is released**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things.
|
8
|
-
|
9
5
|
The gem will automatically apply several headers that are related to security. This includes:
|
10
6
|
- Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/)
|
11
7
|
- https://csp.withgoogle.com
|
@@ -33,20 +29,6 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu
|
|
33
29
|
- [Hashes](docs/hashes.md)
|
34
30
|
- [Sinatra Config](docs/sinatra.md)
|
35
31
|
|
36
|
-
## Getting Started
|
37
|
-
|
38
|
-
### Rails 3+
|
39
|
-
|
40
|
-
For Rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. If for some reason the middleware is not being included follow the instructions for Rails 2.
|
41
|
-
|
42
|
-
### Rails 2
|
43
|
-
|
44
|
-
For Rails 2 or non-rails applications, an explicit statement is required to use the middleware component.
|
45
|
-
|
46
|
-
```ruby
|
47
|
-
use SecureHeaders::Middleware
|
48
|
-
```
|
49
|
-
|
50
32
|
## Configuration
|
51
33
|
|
52
34
|
If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block.
|
@@ -119,6 +101,23 @@ X-Permitted-Cross-Domain-Policies: none
|
|
119
101
|
X-Xss-Protection: 1; mode=block
|
120
102
|
```
|
121
103
|
|
104
|
+
## API configurations
|
105
|
+
|
106
|
+
Which headers you decide to use for API responses is entirely a personal choice. Things like X-Frame-Options seem to have no place in an API response and would be wasting bytes. While this is true, browsers can do funky things with non-html responses. At the minimum, we suggest CSP:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
SecureHeaders::Configuration.override(:api) do |config|
|
110
|
+
config.csp = { default_src: 'none' }
|
111
|
+
config.hsts = SecureHeaders::OPT_OUT
|
112
|
+
config.x_frame_options = SecureHeaders::OPT_OUT
|
113
|
+
config.x_content_type_options = SecureHeaders::OPT_OUT
|
114
|
+
config.x_xss_protection = SecureHeaders::OPT_OUT
|
115
|
+
config.x_permitted_cross_domain_policies = SecureHeaders::OPT_OUT
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
However, I would consider these headers anyways depending on your load and bandwidth requirements.
|
120
|
+
|
122
121
|
## Similar libraries
|
123
122
|
|
124
123
|
* Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers)
|
data/lib/secure_headers.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "secure_headers/hash_helper"
|
3
3
|
require "secure_headers/headers/cookie"
|
4
|
-
require "secure_headers/headers/public_key_pins"
|
5
4
|
require "secure_headers/headers/content_security_policy"
|
6
5
|
require "secure_headers/headers/x_frame_options"
|
7
6
|
require "secure_headers/headers/strict_transport_security"
|
@@ -18,8 +17,7 @@ require "secure_headers/view_helper"
|
|
18
17
|
require "singleton"
|
19
18
|
require "secure_headers/configuration"
|
20
19
|
|
21
|
-
#
|
22
|
-
# or ":optout_of_protection" as a config value to disable a given header
|
20
|
+
# Provide SecureHeaders::OPT_OUT as a config value to disable a given header
|
23
21
|
module SecureHeaders
|
24
22
|
class NoOpHeaderConfig
|
25
23
|
include Singleton
|
@@ -51,10 +49,6 @@ module SecureHeaders
|
|
51
49
|
HTTPS = "https".freeze
|
52
50
|
CSP = ContentSecurityPolicy
|
53
51
|
|
54
|
-
# Headers set on http requests (excludes STS and HPKP)
|
55
|
-
HTTPS_HEADER_CLASSES =
|
56
|
-
[StrictTransportSecurity, PublicKeyPins].freeze
|
57
|
-
|
58
52
|
class << self
|
59
53
|
# Public: override a given set of directives for the current request. If a
|
60
54
|
# value already exists for a given directive, it will be overridden.
|
@@ -138,7 +132,7 @@ module SecureHeaders
|
|
138
132
|
# Public: Builds the hash of headers that should be applied base on the
|
139
133
|
# request.
|
140
134
|
#
|
141
|
-
# StrictTransportSecurity
|
135
|
+
# StrictTransportSecurity is not applied to http requests.
|
142
136
|
# See #config_for to determine which config is used for a given request.
|
143
137
|
#
|
144
138
|
# Returns a hash of header names => header values. The value
|
@@ -151,9 +145,7 @@ module SecureHeaders
|
|
151
145
|
headers = config.generate_headers
|
152
146
|
|
153
147
|
if request.scheme != HTTPS
|
154
|
-
|
155
|
-
headers.delete(klass::HEADER_NAME)
|
156
|
-
end
|
148
|
+
headers.delete(StrictTransportSecurity::HEADER_NAME)
|
157
149
|
end
|
158
150
|
headers
|
159
151
|
end
|
@@ -115,7 +115,6 @@ module SecureHeaders
|
|
115
115
|
expect_certificate_transparency: ExpectCertificateTransparency,
|
116
116
|
csp: ContentSecurityPolicy,
|
117
117
|
csp_report_only: ContentSecurityPolicy,
|
118
|
-
hpkp: PublicKeyPins,
|
119
118
|
cookies: Cookie,
|
120
119
|
}.freeze
|
121
120
|
|
@@ -144,7 +143,6 @@ module SecureHeaders
|
|
144
143
|
@clear_site_data = nil
|
145
144
|
@csp = nil
|
146
145
|
@csp_report_only = nil
|
147
|
-
@hpkp = nil
|
148
146
|
@hsts = nil
|
149
147
|
@x_content_type_options = nil
|
150
148
|
@x_download_options = nil
|
@@ -153,7 +151,6 @@ module SecureHeaders
|
|
153
151
|
@x_xss_protection = nil
|
154
152
|
@expect_certificate_transparency = nil
|
155
153
|
|
156
|
-
self.hpkp = OPT_OUT
|
157
154
|
self.referrer_policy = OPT_OUT
|
158
155
|
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
159
156
|
self.csp_report_only = OPT_OUT
|
@@ -178,7 +175,6 @@ module SecureHeaders
|
|
178
175
|
copy.clear_site_data = @clear_site_data
|
179
176
|
copy.expect_certificate_transparency = @expect_certificate_transparency
|
180
177
|
copy.referrer_policy = @referrer_policy
|
181
|
-
copy.hpkp = @hpkp
|
182
178
|
copy
|
183
179
|
end
|
184
180
|
|
@@ -263,10 +259,5 @@ module SecureHeaders
|
|
263
259
|
raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash"
|
264
260
|
end
|
265
261
|
end
|
266
|
-
|
267
|
-
def hpkp_report_host
|
268
|
-
return nil unless @hpkp && hpkp != OPT_OUT && @hpkp[:report_uri]
|
269
|
-
URI.parse(@hpkp[:report_uri]).host
|
270
|
-
end
|
271
262
|
end
|
272
263
|
end
|
@@ -51,14 +51,14 @@ module SecureHeaders
|
|
51
51
|
def build_value
|
52
52
|
directives.map do |directive_name|
|
53
53
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
54
|
+
when :source_list, :require_sri_for_list # require_sri is a simple set of strings that don't need to deal with symbol casing
|
55
|
+
build_source_list_directive(directive_name)
|
54
56
|
when :boolean
|
55
57
|
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
56
58
|
when :sandbox_list
|
57
59
|
build_sandbox_list_directive(directive_name)
|
58
60
|
when :media_type_list
|
59
61
|
build_media_type_list_directive(directive_name)
|
60
|
-
when :source_list
|
61
|
-
build_source_list_directive(directive_name)
|
62
62
|
end
|
63
63
|
end.compact.join("; ")
|
64
64
|
end
|
@@ -27,11 +27,14 @@ module SecureHeaders
|
|
27
27
|
@img_src = nil
|
28
28
|
@manifest_src = nil
|
29
29
|
@media_src = nil
|
30
|
+
@navigate_to = nil
|
30
31
|
@object_src = nil
|
31
32
|
@plugin_types = nil
|
33
|
+
@prefetch_src = nil
|
32
34
|
@preserve_schemes = nil
|
33
35
|
@report_only = nil
|
34
36
|
@report_uri = nil
|
37
|
+
@require_sri_for = nil
|
35
38
|
@sandbox = nil
|
36
39
|
@script_nonce = nil
|
37
40
|
@script_src = nil
|
@@ -1,4 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
2
5
|
module SecureHeaders
|
3
6
|
module PolicyManagement
|
4
7
|
def self.included(base)
|
@@ -70,6 +73,9 @@ module SecureHeaders
|
|
70
73
|
# https://w3c.github.io/webappsec/specs/CSP2/
|
71
74
|
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
72
75
|
MANIFEST_SRC = :manifest_src
|
76
|
+
NAVIGATE_TO = :navigate_to
|
77
|
+
PREFETCH_SRC = :prefetch_src
|
78
|
+
REQUIRE_SRI_FOR = :require_sri_for
|
73
79
|
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
74
80
|
WORKER_SRC = :worker_src
|
75
81
|
|
@@ -77,6 +83,9 @@ module SecureHeaders
|
|
77
83
|
DIRECTIVES_2_0,
|
78
84
|
BLOCK_ALL_MIXED_CONTENT,
|
79
85
|
MANIFEST_SRC,
|
86
|
+
NAVIGATE_TO,
|
87
|
+
PREFETCH_SRC,
|
88
|
+
REQUIRE_SRI_FOR,
|
80
89
|
WORKER_SRC,
|
81
90
|
UPGRADE_INSECURE_REQUESTS
|
82
91
|
].flatten.freeze
|
@@ -100,14 +109,17 @@ module SecureHeaders
|
|
100
109
|
IMG_SRC => :source_list,
|
101
110
|
MANIFEST_SRC => :source_list,
|
102
111
|
MEDIA_SRC => :source_list,
|
112
|
+
NAVIGATE_TO => :source_list,
|
103
113
|
OBJECT_SRC => :source_list,
|
104
114
|
PLUGIN_TYPES => :media_type_list,
|
115
|
+
REQUIRE_SRI_FOR => :require_sri_for_list,
|
105
116
|
REPORT_URI => :source_list,
|
117
|
+
PREFETCH_SRC => :source_list,
|
106
118
|
SANDBOX => :sandbox_list,
|
107
119
|
SCRIPT_SRC => :source_list,
|
108
120
|
STYLE_SRC => :source_list,
|
109
121
|
WORKER_SRC => :source_list,
|
110
|
-
UPGRADE_INSECURE_REQUESTS => :boolean
|
122
|
+
UPGRADE_INSECURE_REQUESTS => :boolean,
|
111
123
|
}.freeze
|
112
124
|
|
113
125
|
# These are directives that don't have use a source list, and hence do not
|
@@ -122,7 +134,8 @@ module SecureHeaders
|
|
122
134
|
BASE_URI,
|
123
135
|
FORM_ACTION,
|
124
136
|
FRAME_ANCESTORS,
|
125
|
-
|
137
|
+
NAVIGATE_TO,
|
138
|
+
REPORT_URI,
|
126
139
|
]
|
127
140
|
|
128
141
|
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
|
@@ -148,6 +161,8 @@ module SecureHeaders
|
|
148
161
|
:style_nonce
|
149
162
|
].freeze
|
150
163
|
|
164
|
+
REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style))
|
165
|
+
|
151
166
|
module ClassMethods
|
152
167
|
# Public: generate a header name, value array that is user-agent-aware.
|
153
168
|
#
|
@@ -241,7 +256,8 @@ module SecureHeaders
|
|
241
256
|
def list_directive?(directive)
|
242
257
|
source_list?(directive) ||
|
243
258
|
sandbox_list?(directive) ||
|
244
|
-
media_type_list?(directive)
|
259
|
+
media_type_list?(directive) ||
|
260
|
+
require_sri_for_list?(directive)
|
245
261
|
end
|
246
262
|
|
247
263
|
# For each directive in additions that does not exist in the original config,
|
@@ -274,11 +290,17 @@ module SecureHeaders
|
|
274
290
|
DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
|
275
291
|
end
|
276
292
|
|
293
|
+
def require_sri_for_list?(directive)
|
294
|
+
DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list
|
295
|
+
end
|
296
|
+
|
277
297
|
# Private: Validates that the configuration has a valid type, or that it is a valid
|
278
298
|
# source expression.
|
279
299
|
def validate_directive!(directive, value)
|
280
300
|
ensure_valid_directive!(directive)
|
281
301
|
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
|
302
|
+
when :source_list
|
303
|
+
validate_source_expression!(directive, value)
|
282
304
|
when :boolean
|
283
305
|
unless boolean?(value)
|
284
306
|
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
|
@@ -287,8 +309,8 @@ module SecureHeaders
|
|
287
309
|
validate_sandbox_expression!(directive, value)
|
288
310
|
when :media_type_list
|
289
311
|
validate_media_type_expression!(directive, value)
|
290
|
-
when :
|
291
|
-
|
312
|
+
when :require_sri_for_list
|
313
|
+
validate_require_sri_source_expression!(directive, value)
|
292
314
|
else
|
293
315
|
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
|
294
316
|
end
|
@@ -323,6 +345,16 @@ module SecureHeaders
|
|
323
345
|
end
|
324
346
|
end
|
325
347
|
|
348
|
+
# Private: validates that a require sri for expression:
|
349
|
+
# 1. is an array of strings
|
350
|
+
# 2. is a subset of ["string", "style"]
|
351
|
+
def validate_require_sri_source_expression!(directive, require_sri_for_expression)
|
352
|
+
ensure_array_of_strings!(directive, require_sri_for_expression)
|
353
|
+
unless require_sri_for_expression.to_set.subset?(REQUIRE_SRI_FOR_VALUES)
|
354
|
+
raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_SRI_FOR_VALUES.to_a} but was #{require_sri_for_expression}))
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
326
358
|
# Private: validates that a source expression:
|
327
359
|
# 1. is an array of strings
|
328
360
|
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module SecureHeaders
|
3
3
|
class Middleware
|
4
|
-
HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166"
|
5
|
-
|
6
4
|
def initialize(app)
|
7
5
|
@app = app
|
8
6
|
end
|
@@ -13,10 +11,6 @@ module SecureHeaders
|
|
13
11
|
status, headers, response = @app.call(env)
|
14
12
|
|
15
13
|
config = SecureHeaders.config_for(req)
|
16
|
-
if config.hpkp_report_host == req.host
|
17
|
-
Kernel.warn(HPKP_SAME_HOST_WARNING)
|
18
|
-
end
|
19
|
-
|
20
14
|
flag_cookies!(headers, override_secure(env, config.cookies)) unless config.cookies == OPT_OUT
|
21
15
|
headers.merge!(SecureHeaders.header_hash_for(req))
|
22
16
|
[status, headers, response]
|
@@ -65,7 +65,7 @@ module SecureHeaders
|
|
65
65
|
|
66
66
|
def validate_hash_or_true_or_opt_out!(attribute)
|
67
67
|
if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute]))
|
68
|
-
raise CookiesConfigError.new("#{attribute} cookie config must be a hash or
|
68
|
+
raise CookiesConfigError.new("#{attribute} cookie config must be a hash, true, or SecureHeaders::OPT_OUT")
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
data/secure_headers.gemspec
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "secure_headers/version"
|
4
|
+
|
1
5
|
# -*- encoding: utf-8 -*-
|
2
6
|
# frozen_string_literal: true
|
3
7
|
Gem::Specification.new do |gem|
|
4
8
|
gem.name = "secure_headers"
|
5
|
-
gem.version =
|
9
|
+
gem.version = SecureHeaders::VERSION
|
6
10
|
gem.authors = ["Neil Matatall"]
|
7
11
|
gem.email = ["neil.matatall@gmail.com"]
|
8
12
|
gem.description = "Manages application of security headers with many safe defaults."
|
@@ -116,6 +116,31 @@ module SecureHeaders
|
|
116
116
|
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
117
117
|
end
|
118
118
|
|
119
|
+
it "allows script as a require-sri-src" do
|
120
|
+
csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script))
|
121
|
+
expect(csp.value).to eq("default-src 'self'; require-sri-for script")
|
122
|
+
end
|
123
|
+
|
124
|
+
it "allows style as a require-sri-src" do
|
125
|
+
csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(style))
|
126
|
+
expect(csp.value).to eq("default-src 'self'; require-sri-for style")
|
127
|
+
end
|
128
|
+
|
129
|
+
it "allows script and style as a require-sri-src" do
|
130
|
+
csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script style))
|
131
|
+
expect(csp.value).to eq("default-src 'self'; require-sri-for script style")
|
132
|
+
end
|
133
|
+
|
134
|
+
it "includes prefetch-src" do
|
135
|
+
csp = ContentSecurityPolicy.new(default_src: %w('self'), prefetch_src: %w(foo.com))
|
136
|
+
expect(csp.value).to eq("default-src 'self'; prefetch-src foo.com")
|
137
|
+
end
|
138
|
+
|
139
|
+
it "includes navigate-to" do
|
140
|
+
csp = ContentSecurityPolicy.new(default_src: %w('self'), navigate_to: %w(foo.com))
|
141
|
+
expect(csp.value).to eq("default-src 'self'; navigate-to foo.com")
|
142
|
+
end
|
143
|
+
|
119
144
|
it "supports strict-dynamic" do
|
120
145
|
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456})
|
121
146
|
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
|
@@ -28,24 +28,29 @@ module SecureHeaders
|
|
28
28
|
|
29
29
|
# directive values: these values will directly translate into source directives
|
30
30
|
default_src: %w(https: 'self'),
|
31
|
-
|
32
|
-
|
31
|
+
|
32
|
+
base_uri: %w('self'),
|
33
|
+
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
|
33
34
|
connect_src: %w(wss:),
|
35
|
+
child_src: %w('self' *.twimg.com itunes.apple.com),
|
34
36
|
font_src: %w('self' data:),
|
37
|
+
form_action: %w('self' github.com),
|
38
|
+
frame_ancestors: %w('none'),
|
39
|
+
frame_src: %w('self' *.twimg.com itunes.apple.com),
|
35
40
|
img_src: %w(mycdn.com data:),
|
36
41
|
manifest_src: %w(manifest.com),
|
37
42
|
media_src: %w(utoob.com),
|
43
|
+
navigate_to: %w(netscape.com),
|
38
44
|
object_src: %w('self'),
|
45
|
+
plugin_types: %w(application/x-shockwave-flash),
|
46
|
+
prefetch_src: %w(fetch.com),
|
47
|
+
require_sri_for: %w(script style),
|
39
48
|
script_src: %w('self'),
|
40
49
|
style_src: %w('unsafe-inline'),
|
41
|
-
worker_src: %w(worker.com),
|
42
|
-
base_uri: %w('self'),
|
43
|
-
form_action: %w('self' github.com),
|
44
|
-
frame_ancestors: %w('none'),
|
45
|
-
plugin_types: %w(application/x-shockwave-flash),
|
46
|
-
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
|
47
50
|
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
48
|
-
|
51
|
+
worker_src: %w(worker.com),
|
52
|
+
|
53
|
+
report_uri: %w(https://example.com/uri-directive),
|
49
54
|
}
|
50
55
|
|
51
56
|
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config))
|
@@ -14,25 +14,6 @@ module SecureHeaders
|
|
14
14
|
Configuration.default
|
15
15
|
end
|
16
16
|
|
17
|
-
it "warns if the hpkp report-uri host is the same as the current host" do
|
18
|
-
report_host = "report-uri.io"
|
19
|
-
reset_config
|
20
|
-
Configuration.default do |config|
|
21
|
-
config.hpkp = {
|
22
|
-
max_age: 10000000,
|
23
|
-
pins: [
|
24
|
-
{sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
|
25
|
-
{sha256: "73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f"}
|
26
|
-
],
|
27
|
-
report_uri: "https://#{report_host}/example-hpkp"
|
28
|
-
}
|
29
|
-
end
|
30
|
-
|
31
|
-
expect(Kernel).to receive(:warn).with(Middleware::HPKP_SAME_HOST_WARNING)
|
32
|
-
|
33
|
-
middleware.call(Rack::MockRequest.env_for("https://#{report_host}", {}))
|
34
|
-
end
|
35
|
-
|
36
17
|
it "sets the headers" do
|
37
18
|
_, env = middleware.call(Rack::MockRequest.env_for("https://looocalhost", {}))
|
38
19
|
expect_default_values(env)
|
@@ -90,16 +90,6 @@ module SecureHeaders
|
|
90
90
|
Configuration.default do |config|
|
91
91
|
config.csp = { default_src: ["example.com"], script_src: %w('self') }
|
92
92
|
config.csp_report_only = config.csp
|
93
|
-
config.hpkp = {
|
94
|
-
report_only: false,
|
95
|
-
max_age: 10000000,
|
96
|
-
include_subdomains: true,
|
97
|
-
report_uri: "https://report-uri.io/example-hpkp",
|
98
|
-
pins: [
|
99
|
-
{sha256: "abc"},
|
100
|
-
{sha256: "123"}
|
101
|
-
]
|
102
|
-
}
|
103
93
|
end
|
104
94
|
SecureHeaders.opt_out_of_all_protection(request)
|
105
95
|
hash = SecureHeaders.header_hash_for(request)
|
@@ -141,23 +131,6 @@ module SecureHeaders
|
|
141
131
|
expect(SecureHeaders.header_hash_for(plaintext_request)[StrictTransportSecurity::HEADER_NAME]).to be_nil
|
142
132
|
end
|
143
133
|
|
144
|
-
it "does not set the HPKP header if request is over HTTP" do
|
145
|
-
plaintext_request = Rack::Request.new({})
|
146
|
-
Configuration.default do |config|
|
147
|
-
config.hpkp = {
|
148
|
-
max_age: 1_000_000,
|
149
|
-
include_subdomains: true,
|
150
|
-
report_uri: "//example.com/uri-directive",
|
151
|
-
pins: [
|
152
|
-
{ sha256: "abc" },
|
153
|
-
{ sha256: "123" }
|
154
|
-
]
|
155
|
-
}
|
156
|
-
end
|
157
|
-
|
158
|
-
expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil
|
159
|
-
end
|
160
|
-
|
161
134
|
context "content security policy" do
|
162
135
|
let(:chrome_request) {
|
163
136
|
Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
@@ -531,14 +504,6 @@ module SecureHeaders
|
|
531
504
|
end.to raise_error(ReferrerPolicyConfigError)
|
532
505
|
end
|
533
506
|
|
534
|
-
it "validates your hpkp config upon configuration" do
|
535
|
-
expect do
|
536
|
-
Configuration.default do |config|
|
537
|
-
config.hpkp = "lol"
|
538
|
-
end
|
539
|
-
end.to raise_error(PublicKeyPinsConfigError)
|
540
|
-
end
|
541
|
-
|
542
507
|
it "validates your cookies config upon configuration" do
|
543
508
|
expect do
|
544
509
|
Configuration.default do |config|
|
data/spec/spec_helper.rb
CHANGED
@@ -37,7 +37,6 @@ def expect_default_values(hash)
|
|
37
37
|
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
|
38
38
|
expect(hash[SecureHeaders::ClearSiteData::HEADER_NAME]).to be_nil
|
39
39
|
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
|
40
|
-
expect(hash[SecureHeaders::PublicKeyPins::HEADER_NAME]).to be_nil
|
41
40
|
end
|
42
41
|
|
43
42
|
module SecureHeaders
|
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.
|
4
|
+
version: 6.1.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:
|
11
|
+
date: 2019-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -65,7 +65,6 @@ files:
|
|
65
65
|
- lib/secure_headers/headers/cookie.rb
|
66
66
|
- lib/secure_headers/headers/expect_certificate_transparency.rb
|
67
67
|
- lib/secure_headers/headers/policy_management.rb
|
68
|
-
- lib/secure_headers/headers/public_key_pins.rb
|
69
68
|
- lib/secure_headers/headers/referrer_policy.rb
|
70
69
|
- lib/secure_headers/headers/strict_transport_security.rb
|
71
70
|
- lib/secure_headers/headers/x_content_type_options.rb
|
@@ -76,6 +75,7 @@ files:
|
|
76
75
|
- lib/secure_headers/middleware.rb
|
77
76
|
- lib/secure_headers/railtie.rb
|
78
77
|
- lib/secure_headers/utils/cookies_config.rb
|
78
|
+
- lib/secure_headers/version.rb
|
79
79
|
- lib/secure_headers/view_helper.rb
|
80
80
|
- lib/tasks/tasks.rake
|
81
81
|
- secure_headers.gemspec
|
@@ -85,7 +85,6 @@ files:
|
|
85
85
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
86
86
|
- spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb
|
87
87
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
88
|
-
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
89
88
|
- spec/lib/secure_headers/headers/referrer_policy_spec.rb
|
90
89
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
91
90
|
- spec/lib/secure_headers/headers/x_content_type_options_spec.rb
|
@@ -116,8 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
115
|
- !ruby/object:Gem::Version
|
117
116
|
version: '0'
|
118
117
|
requirements: []
|
119
|
-
|
120
|
-
rubygems_version: 2.6.13
|
118
|
+
rubygems_version: 3.0.1
|
121
119
|
signing_key:
|
122
120
|
specification_version: 4
|
123
121
|
summary: Add easily configured security headers to responses including content-security-policy,
|
@@ -129,7 +127,6 @@ test_files:
|
|
129
127
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
130
128
|
- spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb
|
131
129
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
132
|
-
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
133
130
|
- spec/lib/secure_headers/headers/referrer_policy_spec.rb
|
134
131
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
135
132
|
- spec/lib/secure_headers/headers/x_content_type_options_spec.rb
|
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module SecureHeaders
|
3
|
-
class PublicKeyPinsConfigError < StandardError; end
|
4
|
-
class PublicKeyPins
|
5
|
-
HEADER_NAME = "Public-Key-Pins".freeze
|
6
|
-
REPORT_ONLY = "Public-Key-Pins-Report-Only".freeze
|
7
|
-
HASH_ALGORITHMS = [:sha256].freeze
|
8
|
-
|
9
|
-
|
10
|
-
class << self
|
11
|
-
# Public: make an hpkp header name, value pair
|
12
|
-
#
|
13
|
-
# Returns nil if not configured, returns header name and value if configured.
|
14
|
-
def make_header(config, user_agent = nil)
|
15
|
-
return if config.nil? || config == OPT_OUT
|
16
|
-
header = new(config)
|
17
|
-
[header.name, header.value]
|
18
|
-
end
|
19
|
-
|
20
|
-
def validate_config!(config)
|
21
|
-
return if config.nil? || config == OPT_OUT
|
22
|
-
raise PublicKeyPinsConfigError.new("config must be a hash.") unless config.is_a? Hash
|
23
|
-
|
24
|
-
if !config[:max_age]
|
25
|
-
raise PublicKeyPinsConfigError.new("max-age is a required directive.")
|
26
|
-
elsif config[:max_age].to_s !~ /\A\d+\z/
|
27
|
-
raise PublicKeyPinsConfigError.new("max-age must be a number.
|
28
|
-
#{config[:max_age]} was supplied.")
|
29
|
-
elsif config[:pins] && config[:pins].length < 2
|
30
|
-
raise PublicKeyPinsConfigError.new("A minimum of 2 pins are required.")
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def initialize(config)
|
36
|
-
@max_age = config.fetch(:max_age, nil)
|
37
|
-
@pins = config.fetch(:pins, nil)
|
38
|
-
@report_uri = config.fetch(:report_uri, nil)
|
39
|
-
@report_only = !!config.fetch(:report_only, nil)
|
40
|
-
@include_subdomains = !!config.fetch(:include_subdomains, nil)
|
41
|
-
end
|
42
|
-
|
43
|
-
def name
|
44
|
-
if @report_only
|
45
|
-
REPORT_ONLY
|
46
|
-
else
|
47
|
-
HEADER_NAME
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def value
|
52
|
-
[
|
53
|
-
max_age_directive,
|
54
|
-
pin_directives,
|
55
|
-
report_uri_directive,
|
56
|
-
subdomain_directive
|
57
|
-
].compact.join("; ").strip
|
58
|
-
end
|
59
|
-
|
60
|
-
def pin_directives
|
61
|
-
return nil if @pins.nil?
|
62
|
-
@pins.collect do |pin|
|
63
|
-
pin.map do |token, hash|
|
64
|
-
"pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token)
|
65
|
-
end
|
66
|
-
end.join("; ")
|
67
|
-
end
|
68
|
-
|
69
|
-
def max_age_directive
|
70
|
-
"max-age=#{@max_age}" if @max_age
|
71
|
-
end
|
72
|
-
|
73
|
-
def report_uri_directive
|
74
|
-
"report-uri=\"#{@report_uri}\"" if @report_uri
|
75
|
-
end
|
76
|
-
|
77
|
-
def subdomain_directive
|
78
|
-
@include_subdomains ? "includeSubDomains" : nil
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require "spec_helper"
|
3
|
-
|
4
|
-
module SecureHeaders
|
5
|
-
describe PublicKeyPins do
|
6
|
-
specify { expect(PublicKeyPins.new(max_age: 1234, report_only: true).name).to eq("Public-Key-Pins-Report-Only") }
|
7
|
-
specify { expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins") }
|
8
|
-
|
9
|
-
specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
|
10
|
-
specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
|
11
|
-
specify do
|
12
|
-
config = { max_age: 1234, pins: [{ sha256: "base64encodedpin1" }, { sha256: "base64encodedpin2" }] }
|
13
|
-
header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\""
|
14
|
-
expect(PublicKeyPins.new(config).value).to eq(header_value)
|
15
|
-
end
|
16
|
-
|
17
|
-
context "with an invalid configuration" do
|
18
|
-
it "raises an exception when max-age is not provided" do
|
19
|
-
expect do
|
20
|
-
PublicKeyPins.validate_config!(foo: "bar")
|
21
|
-
end.to raise_error(PublicKeyPinsConfigError)
|
22
|
-
end
|
23
|
-
|
24
|
-
it "raises an exception with an invalid max-age" do
|
25
|
-
expect do
|
26
|
-
PublicKeyPins.validate_config!(max_age: "abc123")
|
27
|
-
end.to raise_error(PublicKeyPinsConfigError)
|
28
|
-
end
|
29
|
-
|
30
|
-
it "raises an exception with less than 2 pins" do
|
31
|
-
expect do
|
32
|
-
config = { max_age: 1234, pins: [{ sha256: "base64encodedpin" }] }
|
33
|
-
PublicKeyPins.validate_config!(config)
|
34
|
-
end.to raise_error(PublicKeyPinsConfigError)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|