secure_headers 2.2.0 → 2.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/.ruby-version +1 -1
- data/.travis.yml +1 -1
- data/Gemfile +3 -2
- data/README.md +89 -18
- data/Rakefile +2 -2
- data/fixtures/{rails_3_2_12_no_init → rails_3_2_22}/Gemfile +2 -1
- data/fixtures/{rails_3_2_12 → rails_3_2_22}/config/initializers/secure_headers.rb +2 -3
- data/fixtures/{rails_3_2_12 → rails_3_2_22}/spec/controllers/other_things_controller_spec.rb +1 -1
- data/fixtures/{rails_3_2_12 → rails_3_2_22_no_init}/Gemfile +2 -2
- data/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb +20 -0
- data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/spec/controllers/other_things_controller_spec.rb +7 -1
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +1 -1
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +2 -3
- data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +1 -1
- data/lib/secure_headers/headers/content_security_policy.rb +41 -41
- data/lib/secure_headers/headers/public_key_pins.rb +1 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +1 -0
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
- data/lib/secure_headers/headers/x_download_options.rb +1 -0
- data/lib/secure_headers/headers/x_frame_options.rb +1 -0
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -0
- data/lib/secure_headers/headers/x_xss_protection.rb +1 -0
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers.rb +59 -27
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +2 -2
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +64 -36
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +1 -1
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +1 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +1 -1
- data/spec/lib/secure_headers_spec.rb +51 -5
- data/spec/spec_helper.rb +8 -1
- metadata +72 -59
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +0 -6
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/.rspec +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/README.rdoc +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/Rakefile +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/app/controllers/application_controller.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/app/controllers/other_things_controller.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/app/controllers/things_controller.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/app/models/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/app/views/layouts/application.html.erb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/app/views/other_things/index.html.erb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/app/views/things/index.html.erb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/config/application.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/config/boot.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/config/environment.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/config/environments/test.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/config/routes.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/config/script_hashes.yml +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/config.ru +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/lib/assets/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/lib/tasks/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/log/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/spec/controllers/things_controller_spec.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/spec/spec_helper.rb +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/vendor/assets/javascripts/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/vendor/assets/stylesheets/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12 → rails_3_2_22}/vendor/plugins/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/.rspec +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/README.rdoc +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/Rakefile +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/app/controllers/application_controller.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/app/controllers/things_controller.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/app/models/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/app/views/layouts/application.html.erb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/app/views/other_things/index.html.erb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/app/views/things/index.html.erb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/config/application.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/config/boot.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/config/environment.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/config/environments/test.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/config/routes.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/config.ru +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/lib/assets/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/lib/tasks/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/log/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/spec/controllers/things_controller_spec.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/spec/spec_helper.rb +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/vendor/assets/javascripts/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/vendor/assets/stylesheets/.gitkeep +0 -0
- /data/fixtures/{rails_3_2_12_no_init → rails_3_2_22_no_init}/vendor/plugins/.gitkeep +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdc7e4cc47367102f2318244051b544e4bc5d611
|
|
4
|
+
data.tar.gz: f4bb9651462b15c056ab9720a0c079283e9c2462
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 888729b84ba1eb266c25c3bbc218c270f9e262bcbf490363a2daad6202803f4ba71a21f59c1a36e816a06c01ab8d28126ba81e05ae4f37ac36a53ee670af8420
|
|
7
|
+
data.tar.gz: 26640f0a917396faf083eaff557316d4a376e6956ae95915cfb5c815de3269a6ec5cb9a9f6a7684d871d8ff634dc586bb6a6a20dd79ece6cee034d5ec8f20648
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.2.3
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
|
@@ -3,13 +3,14 @@ source 'https://rubygems.org'
|
|
|
3
3
|
gemspec
|
|
4
4
|
|
|
5
5
|
group :test do
|
|
6
|
-
gem '
|
|
6
|
+
gem 'test-unit', '~> 3.0'
|
|
7
|
+
gem 'rails', '3.2.22'
|
|
7
8
|
gem 'sqlite3', :platforms => [:ruby, :mswin, :mingw]
|
|
8
9
|
gem 'jdbc-sqlite3', :platforms => [:jruby]
|
|
9
10
|
gem 'rspec-rails', '>= 3.1'
|
|
10
11
|
gem 'rspec', '>= 3.1'
|
|
11
12
|
gem 'growl'
|
|
12
13
|
gem 'rb-fsevent'
|
|
13
|
-
gem 'coveralls', :platforms => [:ruby_19]
|
|
14
|
+
gem 'coveralls', :platforms => [:ruby_19, :ruby_20, :ruby_21]
|
|
14
15
|
gem 'i18n', '< 0.7.0', :platforms => [:ruby_18]
|
|
15
16
|
end
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The gem will automatically apply several headers that are related to security.
|
|
|
8
8
|
- X-Content-Type-Options - [Prevent content type sniffing](http://msdn.microsoft.com/en-us/library/ie/gg622941\(v=vs.85\).aspx)
|
|
9
9
|
- X-Download-Options - [Prevent file downloads opening](http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx)
|
|
10
10
|
- X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html)
|
|
11
|
-
- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate
|
|
11
|
+
- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinnning Specification](https://tools.ietf.org/html/rfc7469)
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
@@ -29,16 +29,12 @@ The following methods are going to be called, unless they are provided in a `ski
|
|
|
29
29
|
* `:set_x_download_options_header`
|
|
30
30
|
* `:set_x_permitted_cross_domain_policies_header`
|
|
31
31
|
|
|
32
|
-
### Bonus Features
|
|
33
|
-
|
|
34
|
-
This gem makes a few assumptions about how you will use some features. For example:
|
|
35
|
-
|
|
36
|
-
* It fills any blank directives with the value in `:default_src` Getting a default\-src report is pretty useless. This way, you will always know what type of violation occurred. You can disable this feature by supplying `:disable_fill_missing => true`. This is referred to as the "effective-directive" in the spec, but is not well supported as of Nov 5, 2013.
|
|
37
|
-
|
|
38
32
|
## Configuration
|
|
39
33
|
|
|
40
34
|
**Place the following in an initializer (recommended):**
|
|
41
35
|
|
|
36
|
+
**NOTE: All CSP config values accept procs for one way of dynamically setting values**
|
|
37
|
+
|
|
42
38
|
```ruby
|
|
43
39
|
::SecureHeaders::Configuration.configure do |config|
|
|
44
40
|
config.hsts = {:max_age => 20.years.to_i, :include_subdomains => true}
|
|
@@ -48,10 +44,23 @@ This gem makes a few assumptions about how you will use some features. For exam
|
|
|
48
44
|
config.x_download_options = 'noopen'
|
|
49
45
|
config.x_permitted_cross_domain_policies = 'none'
|
|
50
46
|
config.csp = {
|
|
51
|
-
:default_src => "https: self",
|
|
52
|
-
:enforce => proc {|controller|
|
|
47
|
+
:default_src => "https: 'self'",
|
|
48
|
+
:enforce => proc {|controller| controller.my_feature_flag_api.enabled? },
|
|
53
49
|
:frame_src => "https: http:.twimg.com http://itunes.apple.com",
|
|
54
50
|
:img_src => "https:",
|
|
51
|
+
:connect_src => "wws:"
|
|
52
|
+
:font_src => "'self' data:",
|
|
53
|
+
:frame_src => "'self'",
|
|
54
|
+
:img_src => "mycdn.com data:",
|
|
55
|
+
:media_src => "utoob.com",
|
|
56
|
+
:object_src => "'self'",
|
|
57
|
+
:script_src => "'self'",
|
|
58
|
+
:style_src => "'unsafe-inline'",
|
|
59
|
+
:base_uri => "'self'",
|
|
60
|
+
:child_src => "'self'",
|
|
61
|
+
:form_action => "'self' github.com",
|
|
62
|
+
:frame_ancestors => "'none'",
|
|
63
|
+
:plugin_types => 'application/x-shockwave-flash',
|
|
55
64
|
:report_uri => '//example.com/uri-directive'
|
|
56
65
|
}
|
|
57
66
|
config.hpkp = {
|
|
@@ -81,6 +90,37 @@ ensure_security_headers(
|
|
|
81
90
|
)
|
|
82
91
|
```
|
|
83
92
|
|
|
93
|
+
## Per-action configuration
|
|
94
|
+
|
|
95
|
+
Sometimes you need to override your content security policy for a given endpoint. Rather than applying the exception globally, you have a few options:
|
|
96
|
+
|
|
97
|
+
1. Use procs as config values as mentioned above.
|
|
98
|
+
1. Specifying `ensure_security_headers csp: ::SecureHeaders::Configuration.csp.merge(script_src: shadyhost.com)` in a descendent controller will override the settings for that controller only.
|
|
99
|
+
1. Override the `secure_header_options_for` class instance method. e.g.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class SomethingController < ApplicationController
|
|
103
|
+
def wumbus
|
|
104
|
+
# gets style-src override
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def diffendoofer
|
|
108
|
+
# does not get style-src override
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def secure_header_options_for(header, options)
|
|
112
|
+
options = super
|
|
113
|
+
if params[:action] == "wumbus"
|
|
114
|
+
if header == :csp
|
|
115
|
+
options.merge(style_src: "'self'")
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
options
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
84
124
|
## Options for ensure\_security\_headers
|
|
85
125
|
|
|
86
126
|
**To disable any of these headers, supply a value of false (e.g. :hsts => false), supplying nil will set the default value**
|
|
@@ -142,7 +182,7 @@ This configuration will likely work for most applications without modification.
|
|
|
142
182
|
```ruby
|
|
143
183
|
# most basic example
|
|
144
184
|
:csp => {
|
|
145
|
-
:default_src => "https: inline eval",
|
|
185
|
+
:default_src => "https: 'unsafe-inline' 'unsafe-eval'",
|
|
146
186
|
:report_uri => '/uri-directive'
|
|
147
187
|
}
|
|
148
188
|
|
|
@@ -158,7 +198,7 @@ This configuration will likely work for most applications without modification.
|
|
|
158
198
|
|
|
159
199
|
# Auction site wants to allow images from anywhere, plugin content from a list of trusted media providers (including a content distribution network), and scripts only from its server hosting sanitized JavaScript
|
|
160
200
|
:csp => {
|
|
161
|
-
:default_src => 'self',
|
|
201
|
+
:default_src => "'self'",
|
|
162
202
|
:img_src => '*',
|
|
163
203
|
:object_src => ['media1.com', 'media2.com', '*.cdn.com'],
|
|
164
204
|
# alternatively (NOT csv) :object_src => 'media1.com media2.com *.cdn.com'
|
|
@@ -197,8 +237,8 @@ Setting a nonce will also set 'unsafe-inline' for browsers that don't support no
|
|
|
197
237
|
|
|
198
238
|
```ruby
|
|
199
239
|
:csp => {
|
|
200
|
-
:default_src => 'self',
|
|
201
|
-
:script_src => 'self nonce
|
|
240
|
+
:default_src => "'self'",
|
|
241
|
+
:script_src => "'self' nonce"
|
|
202
242
|
}
|
|
203
243
|
```
|
|
204
244
|
|
|
@@ -244,7 +284,7 @@ If you only have a few hashes, you can hardcode them for the entire app:
|
|
|
244
284
|
```ruby
|
|
245
285
|
config.csp = {
|
|
246
286
|
:default_src => "https:",
|
|
247
|
-
:script_src => 'self'
|
|
287
|
+
:script_src => "'self'"
|
|
248
288
|
:script_hashes => ['sha1-abc', 'sha1-qwe']
|
|
249
289
|
}
|
|
250
290
|
```
|
|
@@ -254,7 +294,7 @@ The following will work as well, but may not be as clear:
|
|
|
254
294
|
```ruby
|
|
255
295
|
config.csp = {
|
|
256
296
|
:default_src => "https:",
|
|
257
|
-
:script_src => "self 'sha1-qwe'"
|
|
297
|
+
:script_src => "'self' 'sha1-qwe'"
|
|
258
298
|
}
|
|
259
299
|
```
|
|
260
300
|
|
|
@@ -269,7 +309,7 @@ use ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware
|
|
|
269
309
|
```ruby
|
|
270
310
|
config.csp = {
|
|
271
311
|
:default_src => "https:",
|
|
272
|
-
:script_src => 'self',
|
|
312
|
+
:script_src => "'self'",
|
|
273
313
|
:script_hash_middleware => true
|
|
274
314
|
}
|
|
275
315
|
```
|
|
@@ -415,12 +455,43 @@ def before_load
|
|
|
415
455
|
end
|
|
416
456
|
```
|
|
417
457
|
|
|
458
|
+
### Using in rack middleware
|
|
459
|
+
|
|
460
|
+
The `SecureHeaders::header_hash` generates a hash of all header values, which is useful for merging with rack middleware values.
|
|
461
|
+
|
|
462
|
+
```ruby
|
|
463
|
+
class MySecureHeaders
|
|
464
|
+
include SecureHeaders
|
|
465
|
+
def initialize(app)
|
|
466
|
+
@app = app
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def call(env)
|
|
470
|
+
status, headers, response = @app.call(env)
|
|
471
|
+
security_headers = if override?
|
|
472
|
+
SecureHeaders::header_hash(:csp => false) # uses global config, but overrides CSP config
|
|
473
|
+
else
|
|
474
|
+
SecureHeaders::header_hash # uses global config
|
|
475
|
+
end
|
|
476
|
+
[status, headers.merge(security_headers), [response.body]]
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
module Testapp
|
|
481
|
+
class Application < Rails::Application
|
|
482
|
+
config.middleware.use MySecureHeaders
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
```
|
|
486
|
+
|
|
418
487
|
## Similar libraries
|
|
419
488
|
|
|
489
|
+
* Rack [rack-secure_headers](https://github.com/harmoni/rack-secure_headers)
|
|
420
490
|
* Node.js (express) [helmet](https://github.com/evilpacket/helmet) and [hood](https://github.com/seanmonstar/hood)
|
|
421
|
-
*
|
|
491
|
+
* Node.js (hapi) [blankie](https://github.com/nlf/blankie)
|
|
492
|
+
* J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines)
|
|
422
493
|
* ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki)
|
|
423
|
-
* Python - [django-csp](https://github.com/mozilla/django-csp
|
|
494
|
+
* Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security)
|
|
424
495
|
* Go - [secureheader](https://github.com/kr/secureheader)
|
|
425
496
|
|
|
426
497
|
## Authors
|
data/Rakefile
CHANGED
|
@@ -15,7 +15,7 @@ task :default => :all_spec
|
|
|
15
15
|
desc "Run all specs, and test fixture apps"
|
|
16
16
|
task :all_spec => :spec do
|
|
17
17
|
pwd = Dir.pwd
|
|
18
|
-
Dir.chdir 'fixtures/
|
|
18
|
+
Dir.chdir 'fixtures/rails_3_2_22'
|
|
19
19
|
puts Dir.pwd
|
|
20
20
|
str = `bundle install >> /dev/null; bundle exec rspec spec`
|
|
21
21
|
puts str
|
|
@@ -25,7 +25,7 @@ task :all_spec => :spec do
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
Dir.chdir pwd
|
|
28
|
-
Dir.chdir 'fixtures/
|
|
28
|
+
Dir.chdir 'fixtures/rails_3_2_22_no_init'
|
|
29
29
|
puts Dir.pwd
|
|
30
30
|
puts `bundle install >> /dev/null; bundle exec rspec spec`
|
|
31
31
|
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
config.x_xss_protection = {:value => 1, :mode => 'block'}
|
|
6
6
|
config.x_permitted_cross_domain_policies = 'none'
|
|
7
7
|
csp = {
|
|
8
|
-
:default_src => "self",
|
|
9
|
-
:script_src => "self nonce",
|
|
10
|
-
:disable_fill_missing => true,
|
|
8
|
+
:default_src => "'self'",
|
|
9
|
+
:script_src => "'self' nonce",
|
|
11
10
|
:report_uri => 'somewhere',
|
|
12
11
|
:script_hash_middleware => true,
|
|
13
12
|
:enforce => false # false means warnings only
|
data/fixtures/{rails_3_2_12 → rails_3_2_22}/spec/controllers/other_things_controller_spec.rb
RENAMED
|
@@ -13,7 +13,7 @@ describe OtherThingsController, :type => :controller do
|
|
|
13
13
|
options = opts.merge(
|
|
14
14
|
{
|
|
15
15
|
'HTTPS' => 'on',
|
|
16
|
-
'HTTP_USER_AGENT' => "Mozilla/5.0 (
|
|
16
|
+
'HTTP_USER_AGENT' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22"
|
|
17
17
|
}
|
|
18
18
|
)
|
|
19
19
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class OtherThingsController < ApplicationController
|
|
2
|
+
ensure_security_headers :csp => {:default_src => "'self'"}
|
|
3
|
+
def index
|
|
4
|
+
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def other_action
|
|
8
|
+
render :text => 'yooooo'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def secure_header_options_for(header, options)
|
|
12
|
+
if params[:action] == "other_action"
|
|
13
|
+
if header == :csp
|
|
14
|
+
options.merge(:style_src => "'self'")
|
|
15
|
+
end
|
|
16
|
+
else
|
|
17
|
+
options
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -12,11 +12,17 @@ describe OtherThingsController, :type => :controller do
|
|
|
12
12
|
expect(response.headers['X-Frame-Options']).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
it "sets the
|
|
15
|
+
it "sets the CSP header" do
|
|
16
16
|
get :index
|
|
17
17
|
expect(response.headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; img-src 'self' data:;")
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
it "sets per-action values based on secure_header_options_for" do
|
|
21
|
+
# munges :style_src => self into policy
|
|
22
|
+
get :other_action
|
|
23
|
+
expect(response.headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; img-src 'self' data:; style-src 'self';")
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
#mock ssl
|
|
21
27
|
it "sets the Strict-Transport-Security header" do
|
|
22
28
|
request.env['HTTPS'] = 'on'
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
config.x_xss_protection = {:value => 0}
|
|
6
6
|
config.x_permitted_cross_domain_policies = 'none'
|
|
7
7
|
csp = {
|
|
8
|
-
:default_src => "self",
|
|
9
|
-
:script_src => "self nonce",
|
|
10
|
-
:disable_fill_missing => true,
|
|
8
|
+
:default_src => "'self'",
|
|
9
|
+
:script_src => "'self' nonce",
|
|
11
10
|
:report_uri => 'somewhere',
|
|
12
11
|
:script_hash_middleware => true,
|
|
13
12
|
:enforce => false # false means warnings only
|
|
@@ -13,7 +13,7 @@ describe OtherThingsController, :type => :controller do
|
|
|
13
13
|
options = opts.merge(
|
|
14
14
|
{
|
|
15
15
|
'HTTPS' => 'on',
|
|
16
|
-
'HTTP_USER_AGENT' => "Mozilla/5.0 (
|
|
16
|
+
'HTTP_USER_AGENT' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22"
|
|
17
17
|
}
|
|
18
18
|
)
|
|
19
19
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require 'uri'
|
|
2
2
|
require 'base64'
|
|
3
3
|
require 'securerandom'
|
|
4
|
+
require 'user_agent_parser'
|
|
5
|
+
require 'json'
|
|
4
6
|
|
|
5
7
|
module SecureHeaders
|
|
6
8
|
class ContentSecurityPolicyBuildError < StandardError; end
|
|
@@ -18,31 +20,25 @@ module SecureHeaders
|
|
|
18
20
|
:media_src,
|
|
19
21
|
:object_src,
|
|
20
22
|
:script_src,
|
|
21
|
-
:style_src
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
NON_DEFAULT_SOURCES = [
|
|
23
|
+
:style_src,
|
|
25
24
|
:base_uri,
|
|
26
25
|
:child_src,
|
|
27
26
|
:form_action,
|
|
28
27
|
:frame_ancestors,
|
|
29
|
-
:plugin_types
|
|
30
|
-
:referrer,
|
|
31
|
-
:reflected_xss
|
|
28
|
+
:plugin_types
|
|
32
29
|
]
|
|
33
30
|
|
|
34
31
|
OTHER = [
|
|
35
32
|
:report_uri
|
|
36
33
|
]
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER
|
|
35
|
+
ALL_DIRECTIVES = DIRECTIVES + OTHER
|
|
36
|
+
CONFIG_KEY = :csp
|
|
41
37
|
end
|
|
38
|
+
|
|
42
39
|
include Constants
|
|
43
40
|
|
|
44
|
-
attr_reader :
|
|
45
|
-
alias :disable_fill_missing? :disable_fill_missing
|
|
41
|
+
attr_reader :ssl_request
|
|
46
42
|
alias :ssl_request? :ssl_request
|
|
47
43
|
|
|
48
44
|
class << self
|
|
@@ -108,7 +104,7 @@ module SecureHeaders
|
|
|
108
104
|
@config = config.inject({}) do |hash, (key, value)|
|
|
109
105
|
config_val = value.respond_to?(:call) ? value.call(@controller) : value
|
|
110
106
|
|
|
111
|
-
if
|
|
107
|
+
if DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
|
|
112
108
|
config_val = config_val.split if config_val.is_a? String
|
|
113
109
|
if config_val.is_a?(Array)
|
|
114
110
|
config_val = config_val.map do |val|
|
|
@@ -124,14 +120,12 @@ module SecureHeaders
|
|
|
124
120
|
@http_additions = @config.delete(:http_additions)
|
|
125
121
|
@app_name = @config.delete(:app_name)
|
|
126
122
|
@report_uri = @config.delete(:report_uri)
|
|
127
|
-
|
|
128
|
-
@disable_fill_missing = !!@config.delete(:disable_fill_missing)
|
|
129
123
|
@enforce = !!@config.delete(:enforce)
|
|
124
|
+
@disable_img_src_data_uri = !!@config.delete(:disable_img_src_data_uri)
|
|
130
125
|
@tag_report_uri = !!@config.delete(:tag_report_uri)
|
|
131
126
|
@script_hashes = @config.delete(:script_hashes) || []
|
|
132
127
|
|
|
133
128
|
add_script_hashes if @script_hashes.any?
|
|
134
|
-
fill_directives unless disable_fill_missing?
|
|
135
129
|
end
|
|
136
130
|
|
|
137
131
|
##
|
|
@@ -164,6 +158,21 @@ module SecureHeaders
|
|
|
164
158
|
end
|
|
165
159
|
end
|
|
166
160
|
|
|
161
|
+
def to_json
|
|
162
|
+
build_value
|
|
163
|
+
@config.to_json.gsub(/(\w+)_src/, "\\1-src")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.from_json(*json_configs)
|
|
167
|
+
json_configs.inject({}) do |combined_config, one_config|
|
|
168
|
+
one_config = one_config.gsub(/(\w+)-src/, "\\1_src")
|
|
169
|
+
config = JSON.parse(one_config, :symbolize_names => true)
|
|
170
|
+
combined_config.merge(config) do |_, lhs, rhs|
|
|
171
|
+
lhs | rhs
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
167
176
|
private
|
|
168
177
|
|
|
169
178
|
def add_script_hashes
|
|
@@ -175,21 +184,10 @@ module SecureHeaders
|
|
|
175
184
|
append_http_additions unless ssl_request?
|
|
176
185
|
header_value = [
|
|
177
186
|
generic_directives,
|
|
178
|
-
non_default_directives,
|
|
179
187
|
report_uri_directive
|
|
180
188
|
].join.strip
|
|
181
189
|
end
|
|
182
190
|
|
|
183
|
-
def fill_directives
|
|
184
|
-
if default = @config[:default_src]
|
|
185
|
-
DIRECTIVES.each do |directive|
|
|
186
|
-
unless @config[directive]
|
|
187
|
-
@config[directive] = default
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
191
|
def append_http_additions
|
|
194
192
|
return unless @http_additions
|
|
195
193
|
@http_additions.each do |k, v|
|
|
@@ -200,13 +198,18 @@ module SecureHeaders
|
|
|
200
198
|
|
|
201
199
|
def translate_dir_value val
|
|
202
200
|
if %w{inline eval}.include?(val)
|
|
201
|
+
warn "[DEPRECATION] using inline/eval may not be supported in the future. Instead use 'unsafe-inline'/'unsafe-eval' instead."
|
|
203
202
|
val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'"
|
|
204
|
-
# self/none are special sources/src-dir-values and need to be quoted
|
|
205
203
|
elsif %{self none}.include?(val)
|
|
204
|
+
warn "[DEPRECATION] using self/none may not be supported in the future. Instead use 'self'/'none' instead."
|
|
206
205
|
"'#{val}'"
|
|
207
206
|
elsif val == 'nonce'
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
if supports_nonces?(@ua)
|
|
208
|
+
self.class.set_nonce(@controller, nonce)
|
|
209
|
+
["'nonce-#{nonce}'", "'unsafe-inline'"]
|
|
210
|
+
else
|
|
211
|
+
"'unsafe-inline'"
|
|
212
|
+
end
|
|
210
213
|
else
|
|
211
214
|
val
|
|
212
215
|
end
|
|
@@ -233,10 +236,11 @@ module SecureHeaders
|
|
|
233
236
|
|
|
234
237
|
def generic_directives
|
|
235
238
|
header_value = ''
|
|
239
|
+
data_uri = @disable_img_src_data_uri ? [] : ["data:"]
|
|
236
240
|
if @config[:img_src]
|
|
237
|
-
@config[:img_src] = @config[:img_src] +
|
|
241
|
+
@config[:img_src] = @config[:img_src] + data_uri unless @config[:img_src].include?('data:')
|
|
238
242
|
else
|
|
239
|
-
@config[:img_src] = @config[:default_src] +
|
|
243
|
+
@config[:img_src] = @config[:default_src] + data_uri
|
|
240
244
|
end
|
|
241
245
|
|
|
242
246
|
DIRECTIVES.each do |directive_name|
|
|
@@ -246,17 +250,13 @@ module SecureHeaders
|
|
|
246
250
|
header_value
|
|
247
251
|
end
|
|
248
252
|
|
|
249
|
-
def non_default_directives
|
|
250
|
-
header_value = ''
|
|
251
|
-
NON_DEFAULT_SOURCES.each do |directive_name|
|
|
252
|
-
header_value += build_directive(directive_name) if @config[directive_name]
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
header_value
|
|
256
|
-
end
|
|
257
|
-
|
|
258
253
|
def build_directive(key)
|
|
259
254
|
"#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
|
|
260
255
|
end
|
|
256
|
+
|
|
257
|
+
def supports_nonces?(user_agent)
|
|
258
|
+
parsed_ua = UserAgentParser.parse(user_agent)
|
|
259
|
+
["Chrome", "Opera", "Firefox"].include?(parsed_ua.family)
|
|
260
|
+
end
|
|
261
261
|
end
|
|
262
262
|
end
|
|
@@ -5,6 +5,7 @@ module SecureHeaders
|
|
|
5
5
|
module Constants
|
|
6
6
|
X_CONTENT_TYPE_OPTIONS_HEADER_NAME = "X-Content-Type-Options"
|
|
7
7
|
DEFAULT_VALUE = "nosniff"
|
|
8
|
+
CONFIG_KEY = :x_content_type_options
|
|
8
9
|
end
|
|
9
10
|
include Constants
|
|
10
11
|
|
|
@@ -37,4 +38,4 @@ module SecureHeaders
|
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
|
-
end
|
|
41
|
+
end
|