secure_headers 6.0.0 → 6.1.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.
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
|