secure_headers 3.0.0 → 3.1.1
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/.gitignore +0 -9
- data/.travis.yml +13 -5
- data/CHANGELOG.md +48 -0
- data/Gemfile +5 -2
- data/README.md +14 -45
- data/lib/secure_headers/configuration.rb +86 -54
- data/lib/secure_headers/headers/content_security_policy.rb +31 -294
- data/lib/secure_headers/headers/policy_management.rb +320 -0
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
- data/lib/secure_headers/middleware.rb +23 -0
- data/lib/secure_headers/railtie.rb +3 -3
- data/lib/secure_headers.rb +37 -63
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +2 -4
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +13 -143
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +190 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +1 -1
- data/spec/lib/secure_headers/middleware_spec.rb +23 -4
- data/spec/lib/secure_headers_spec.rb +101 -41
- data/spec/spec_helper.rb +4 -1
- data/upgrading-to-3-0.md +1 -0
- metadata +5 -4
- data/lib/secure_headers/padrino.rb +0 -13
- data/travis.sh +0 -10
|
@@ -2,11 +2,11 @@ require "spec_helper"
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe Middleware do
|
|
5
|
-
let(:app) {
|
|
5
|
+
let(:app) { lambda { |env| [200, env, "app"] } }
|
|
6
|
+
let(:cookie_app) { lambda { |env| [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } }
|
|
6
7
|
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
end
|
|
8
|
+
let(:middleware) { Middleware.new(app) }
|
|
9
|
+
let(:cookie_middleware) { Middleware.new(cookie_app) }
|
|
10
10
|
|
|
11
11
|
before(:each) do
|
|
12
12
|
reset_config
|
|
@@ -33,8 +33,27 @@ module SecureHeaders
|
|
|
33
33
|
end
|
|
34
34
|
request = Rack::Request.new({})
|
|
35
35
|
SecureHeaders.use_secure_headers_override(request, "my_custom_config")
|
|
36
|
+
expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config"))
|
|
36
37
|
_, env = middleware.call request.env
|
|
37
38
|
expect(env[CSP::HEADER_NAME]).to match("example.org")
|
|
38
39
|
end
|
|
40
|
+
|
|
41
|
+
context "cookies should be flagged" do
|
|
42
|
+
it "flags cookies as secure" do
|
|
43
|
+
Configuration.default { |config| config.secure_cookies = true }
|
|
44
|
+
request = Rack::MockRequest.new(cookie_middleware)
|
|
45
|
+
response = request.get '/'
|
|
46
|
+
expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context "cookies should not be flagged" do
|
|
51
|
+
it "does not flags cookies as secure" do
|
|
52
|
+
Configuration.default { |config| config.secure_cookies = false }
|
|
53
|
+
request = Rack::MockRequest.new(cookie_middleware)
|
|
54
|
+
response = request.get '/'
|
|
55
|
+
expect(response.headers['Set-Cookie']).not_to match(Middleware::SECURE_COOKIE_REGEXP)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
39
58
|
end
|
|
40
59
|
end
|
|
@@ -2,57 +2,49 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe SecureHeaders do
|
|
5
|
-
example_hpkp_config = {
|
|
6
|
-
max_age: 1_000_000,
|
|
7
|
-
include_subdomains: true,
|
|
8
|
-
report_uri: '//example.com/uri-directive',
|
|
9
|
-
pins: [
|
|
10
|
-
{ sha256: 'abc' },
|
|
11
|
-
{ sha256: '123' }
|
|
12
|
-
]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
example_hpkp_config_value = %(max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains)
|
|
16
|
-
|
|
17
5
|
before(:each) do
|
|
18
6
|
reset_config
|
|
19
|
-
@request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on")
|
|
20
7
|
end
|
|
21
8
|
|
|
9
|
+
let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") }
|
|
10
|
+
|
|
22
11
|
it "raises a NotYetConfiguredError if default has not been set" do
|
|
23
12
|
expect do
|
|
24
|
-
SecureHeaders.header_hash_for(
|
|
13
|
+
SecureHeaders.header_hash_for(request)
|
|
25
14
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
26
15
|
end
|
|
27
16
|
|
|
28
17
|
it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
|
|
29
18
|
expect do
|
|
30
|
-
SecureHeaders.opt_out_of_header(
|
|
19
|
+
SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
|
|
31
20
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
32
21
|
end
|
|
33
22
|
|
|
34
23
|
describe "#header_hash_for" do
|
|
35
24
|
it "allows you to opt out of individual headers" do
|
|
36
25
|
Configuration.default
|
|
37
|
-
SecureHeaders.opt_out_of_header(
|
|
38
|
-
|
|
26
|
+
SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
|
|
27
|
+
SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
|
|
28
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
39
29
|
expect(hash['Content-Security-Policy-Report-Only']).to be_nil
|
|
40
30
|
expect(hash['Content-Security-Policy']).to be_nil
|
|
31
|
+
expect(hash['X-Content-Type-Options']).to be_nil
|
|
41
32
|
end
|
|
42
33
|
|
|
43
34
|
it "allows you to opt out entirely" do
|
|
44
35
|
Configuration.default
|
|
45
|
-
SecureHeaders.opt_out_of_all_protection(
|
|
46
|
-
hash = SecureHeaders.header_hash_for(
|
|
36
|
+
SecureHeaders.opt_out_of_all_protection(request)
|
|
37
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
47
38
|
ALL_HEADER_CLASSES.each do |klass|
|
|
48
39
|
expect(hash[klass::CONFIG_KEY]).to be_nil
|
|
49
40
|
end
|
|
41
|
+
expect(hash.count).to eq(0)
|
|
50
42
|
end
|
|
51
43
|
|
|
52
44
|
it "allows you to override X-Frame-Options settings" do
|
|
53
45
|
Configuration.default
|
|
54
|
-
SecureHeaders.override_x_frame_options(
|
|
55
|
-
hash = SecureHeaders.header_hash_for(
|
|
46
|
+
SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY)
|
|
47
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
56
48
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY)
|
|
57
49
|
end
|
|
58
50
|
|
|
@@ -62,17 +54,36 @@ module SecureHeaders
|
|
|
62
54
|
config.csp = OPT_OUT
|
|
63
55
|
end
|
|
64
56
|
|
|
65
|
-
SecureHeaders.override_x_frame_options(
|
|
66
|
-
SecureHeaders.override_content_security_policy_directives(
|
|
57
|
+
SecureHeaders.override_x_frame_options(request, XFrameOptions::SAMEORIGIN)
|
|
58
|
+
SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
|
|
67
59
|
|
|
68
|
-
hash = SecureHeaders.header_hash_for(
|
|
60
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
69
61
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
|
|
70
62
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
71
63
|
end
|
|
72
64
|
|
|
65
|
+
it "produces a UA-specific CSP when overriding (and busting the cache)" do
|
|
66
|
+
config = Configuration.default do |config|
|
|
67
|
+
config.csp = {
|
|
68
|
+
default_src: %w('self'),
|
|
69
|
+
child_src: %w('self'), #unsupported by firefox
|
|
70
|
+
frame_src: %w('self')
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
74
|
+
|
|
75
|
+
# append an unsupported directive
|
|
76
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash))
|
|
77
|
+
# append a supported directive
|
|
78
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
|
|
79
|
+
|
|
80
|
+
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
81
|
+
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
|
|
82
|
+
end
|
|
83
|
+
|
|
73
84
|
it "produces a hash of headers with default config" do
|
|
74
85
|
Configuration.default
|
|
75
|
-
hash = SecureHeaders.header_hash_for(
|
|
86
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
76
87
|
expect_default_values(hash)
|
|
77
88
|
end
|
|
78
89
|
|
|
@@ -87,7 +98,15 @@ module SecureHeaders
|
|
|
87
98
|
it "does not set the HPKP header if request is over HTTP" do
|
|
88
99
|
plaintext_request = Rack::Request.new({})
|
|
89
100
|
Configuration.default do |config|
|
|
90
|
-
config.hpkp =
|
|
101
|
+
config.hpkp = {
|
|
102
|
+
max_age: 1_000_000,
|
|
103
|
+
include_subdomains: true,
|
|
104
|
+
report_uri: '//example.com/uri-directive',
|
|
105
|
+
pins: [
|
|
106
|
+
{ sha256: 'abc' },
|
|
107
|
+
{ sha256: '123' }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
91
110
|
end
|
|
92
111
|
|
|
93
112
|
expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil
|
|
@@ -102,26 +121,67 @@ module SecureHeaders
|
|
|
102
121
|
}
|
|
103
122
|
end
|
|
104
123
|
|
|
105
|
-
SecureHeaders.append_content_security_policy_directives(
|
|
106
|
-
hash = SecureHeaders.header_hash_for(
|
|
124
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
125
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
107
126
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
108
127
|
end
|
|
109
128
|
|
|
129
|
+
it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
|
|
130
|
+
Configuration.default do |config|
|
|
131
|
+
config.csp = {
|
|
132
|
+
default_src: %w('self')
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
expect(CSP).to receive(:idempotent_additions?).once
|
|
137
|
+
|
|
138
|
+
# before an override occurs, the env is empty
|
|
139
|
+
expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil
|
|
140
|
+
|
|
141
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
142
|
+
new_config = SecureHeaders.config_for(request)
|
|
143
|
+
expect(new_config).to_not be(Configuration.get)
|
|
144
|
+
|
|
145
|
+
SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com))
|
|
146
|
+
current_config = SecureHeaders.config_for(request)
|
|
147
|
+
expect(current_config).to be(new_config)
|
|
148
|
+
|
|
149
|
+
SecureHeaders.header_hash_for(request)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do
|
|
153
|
+
default_config = Configuration.default
|
|
154
|
+
expect { default_config.csp = {} }.to raise_error(NoMethodError)
|
|
155
|
+
|
|
156
|
+
# config is frozen
|
|
157
|
+
expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError)
|
|
158
|
+
|
|
159
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
160
|
+
new_config = SecureHeaders.config_for(request)
|
|
161
|
+
expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError)
|
|
162
|
+
|
|
163
|
+
expect do
|
|
164
|
+
new_config.instance_eval do
|
|
165
|
+
new_config.csp = {}
|
|
166
|
+
end
|
|
167
|
+
end.to raise_error(Configuration::IllegalPolicyModificationError)
|
|
168
|
+
end
|
|
169
|
+
|
|
110
170
|
it "overrides individual directives" do
|
|
111
171
|
Configuration.default do |config|
|
|
112
172
|
config.csp = {
|
|
113
173
|
default_src: %w('self')
|
|
114
174
|
}
|
|
115
175
|
end
|
|
116
|
-
SecureHeaders.override_content_security_policy_directives(
|
|
117
|
-
hash = SecureHeaders.header_hash_for(
|
|
176
|
+
SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
|
|
177
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
118
178
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'")
|
|
119
179
|
end
|
|
120
180
|
|
|
121
181
|
it "overrides non-existant directives" do
|
|
122
182
|
Configuration.default
|
|
123
|
-
SecureHeaders.override_content_security_policy_directives(
|
|
124
|
-
hash = SecureHeaders.header_hash_for(
|
|
183
|
+
SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
184
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
125
185
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
|
|
126
186
|
end
|
|
127
187
|
|
|
@@ -134,9 +194,9 @@ module SecureHeaders
|
|
|
134
194
|
}
|
|
135
195
|
end
|
|
136
196
|
|
|
137
|
-
|
|
138
|
-
nonce = SecureHeaders.content_security_policy_script_nonce(
|
|
139
|
-
hash = SecureHeaders.header_hash_for(
|
|
197
|
+
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
|
|
198
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
199
|
+
hash = SecureHeaders.header_hash_for(safari_request)
|
|
140
200
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
|
|
141
201
|
end
|
|
142
202
|
|
|
@@ -149,15 +209,15 @@ module SecureHeaders
|
|
|
149
209
|
}
|
|
150
210
|
end
|
|
151
211
|
|
|
152
|
-
|
|
153
|
-
nonce = SecureHeaders.content_security_policy_script_nonce(
|
|
212
|
+
chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
|
213
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
154
214
|
|
|
155
215
|
# simulate the nonce being used multiple times in a request:
|
|
156
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
157
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
158
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
216
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
217
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
218
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
159
219
|
|
|
160
|
-
hash = SecureHeaders.header_hash_for(
|
|
220
|
+
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
161
221
|
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
|
|
162
222
|
end
|
|
163
223
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -3,9 +3,12 @@ require 'rspec'
|
|
|
3
3
|
require 'rack'
|
|
4
4
|
require 'pry-nav'
|
|
5
5
|
|
|
6
|
+
require 'coveralls'
|
|
7
|
+
Coveralls.wear!
|
|
8
|
+
|
|
6
9
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
USER_AGENTS = {
|
|
11
14
|
firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
|
data/upgrading-to-3-0.md
CHANGED
|
@@ -20,6 +20,7 @@ Migrating to 3.x from <= 2.x
|
|
|
20
20
|
1. Convert all instances of `self`/`none`/`eval`/`inline` to the corresponding values in the above table.
|
|
21
21
|
1. Convert all CSP space-delimited directives to an array of strings.
|
|
22
22
|
1. Convert all `enforce: true|false` to `report_only: true|false`.
|
|
23
|
+
1. Remove `ensure_security_headers` from controllers (3.x uses a middleware instead).
|
|
23
24
|
|
|
24
25
|
Everything is terrible, why should I upgrade?
|
|
25
26
|
==
|
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.1.1
|
|
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-03-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- lib/secure_headers.rb
|
|
60
60
|
- lib/secure_headers/configuration.rb
|
|
61
61
|
- lib/secure_headers/headers/content_security_policy.rb
|
|
62
|
+
- lib/secure_headers/headers/policy_management.rb
|
|
62
63
|
- lib/secure_headers/headers/public_key_pins.rb
|
|
63
64
|
- lib/secure_headers/headers/strict_transport_security.rb
|
|
64
65
|
- lib/secure_headers/headers/x_content_type_options.rb
|
|
@@ -67,12 +68,12 @@ files:
|
|
|
67
68
|
- lib/secure_headers/headers/x_permitted_cross_domain_policies.rb
|
|
68
69
|
- lib/secure_headers/headers/x_xss_protection.rb
|
|
69
70
|
- lib/secure_headers/middleware.rb
|
|
70
|
-
- lib/secure_headers/padrino.rb
|
|
71
71
|
- lib/secure_headers/railtie.rb
|
|
72
72
|
- lib/secure_headers/view_helper.rb
|
|
73
73
|
- secure_headers.gemspec
|
|
74
74
|
- spec/lib/secure_headers/configuration_spec.rb
|
|
75
75
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
76
|
+
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
76
77
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
77
78
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
|
78
79
|
- spec/lib/secure_headers/headers/x_content_type_options_spec.rb
|
|
@@ -83,7 +84,6 @@ files:
|
|
|
83
84
|
- spec/lib/secure_headers/middleware_spec.rb
|
|
84
85
|
- spec/lib/secure_headers_spec.rb
|
|
85
86
|
- spec/spec_helper.rb
|
|
86
|
-
- travis.sh
|
|
87
87
|
- upgrading-to-3-0.md
|
|
88
88
|
homepage: https://github.com/twitter/secureheaders
|
|
89
89
|
licenses:
|
|
@@ -113,6 +113,7 @@ summary: Add easily configured security headers to responses including content-s
|
|
|
113
113
|
test_files:
|
|
114
114
|
- spec/lib/secure_headers/configuration_spec.rb
|
|
115
115
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
116
|
+
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
116
117
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
117
118
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
|
118
119
|
- spec/lib/secure_headers/headers/x_content_type_options_spec.rb
|
data/travis.sh
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#! /bin/sh
|
|
2
|
-
|
|
3
|
-
bundle install >> /dev/null &&
|
|
4
|
-
bundle exec rspec --format progress spec &&
|
|
5
|
-
cd fixtures/rails_3_2_12 &&
|
|
6
|
-
bundle install >> /dev/null &&
|
|
7
|
-
bundle exec rspec --format progress spec &&
|
|
8
|
-
cd ../../fixtures/rails_3_2_12_no_init &&
|
|
9
|
-
bundle install >> /dev/null &&
|
|
10
|
-
bundle exec rspec spec
|