secure_headers 2.2.0 → 2.3.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/.travis.yml +0 -1
- data/Gemfile +1 -1
- data/README.md +34 -3
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +1 -1
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +14 -0
- data/fixtures/rails_3_2_12_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/spec/controllers/other_things_controller_spec.rb +1 -1
- data/lib/secure_headers/headers/content_security_policy.rb +34 -4
- 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_spec.rb +52 -2
- data/spec/lib/secure_headers_spec.rb +50 -0
- data/spec/spec_helper.rb +4 -1
- metadata +18 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ea3335e72b8eaea53c9fc03a77c008b73fc674c
|
|
4
|
+
data.tar.gz: 4097b301e8e7f18174abda1c6412f9652cd4c39a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53db0ad5f6d9e7a85bde9aa167a927e3bda91351ce2f57af0f505d1b6c0856ca50b295ad87e26c1b25fa0142a95d8ef8b11f12f562377e60a0b398454abdd5b9
|
|
7
|
+
data.tar.gz: a6c3c2366fcf08ed3749f6c6e9146e2fbc33fb167a504404e195bd9813c709b0d497c2c2fc64f138a482a5fcc68eb4d212895761722ae0beed4b49ccea481e81
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -49,7 +49,7 @@ This gem makes a few assumptions about how you will use some features. For exam
|
|
|
49
49
|
config.x_permitted_cross_domain_policies = 'none'
|
|
50
50
|
config.csp = {
|
|
51
51
|
:default_src => "https: self",
|
|
52
|
-
:enforce => proc {|controller|
|
|
52
|
+
:enforce => proc {|controller| controller.current_user.enforce_csp? },
|
|
53
53
|
:frame_src => "https: http:.twimg.com http://itunes.apple.com",
|
|
54
54
|
:img_src => "https:",
|
|
55
55
|
:report_uri => '//example.com/uri-directive'
|
|
@@ -415,12 +415,43 @@ def before_load
|
|
|
415
415
|
end
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
+
### Using in rack middleware
|
|
419
|
+
|
|
420
|
+
The `SecureHeaders::header_hash` generates a hash of all header values, which is useful for merging with rack middleware values.
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
class MySecureHeaders
|
|
424
|
+
include SecureHeaders
|
|
425
|
+
def initialize(app)
|
|
426
|
+
@app = app
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def call(env)
|
|
430
|
+
status, headers, response = @app.call(env)
|
|
431
|
+
security_headers = if override?
|
|
432
|
+
SecureHeaders::header_hash(:csp => false) # uses global config, but overrides CSP config
|
|
433
|
+
else
|
|
434
|
+
SecureHeaders::header_hash # uses global config
|
|
435
|
+
end
|
|
436
|
+
[status, headers.merge(security_headers), [response.body]]
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
module Testapp
|
|
441
|
+
class Application < Rails::Application
|
|
442
|
+
config.middleware.use MySecureHeaders
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
```
|
|
446
|
+
|
|
418
447
|
## Similar libraries
|
|
419
448
|
|
|
449
|
+
* Rack [rack-secure_headers](https://github.com/harmoni/rack-secure_headers)
|
|
420
450
|
* Node.js (express) [helmet](https://github.com/evilpacket/helmet) and [hood](https://github.com/seanmonstar/hood)
|
|
421
|
-
*
|
|
451
|
+
* Node.js (hapi) [blankie](https://github.com/nlf/blankie)
|
|
452
|
+
* J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines)
|
|
422
453
|
* ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki)
|
|
423
|
-
* Python - [django-csp](https://github.com/mozilla/django-csp
|
|
454
|
+
* Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security)
|
|
424
455
|
* Go - [secureheader](https://github.com/kr/secureheader)
|
|
425
456
|
|
|
426
457
|
## Authors
|
|
@@ -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
|
|
|
@@ -3,4 +3,18 @@ class OtherThingsController < ApplicationController
|
|
|
3
3
|
def index
|
|
4
4
|
|
|
5
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
|
|
6
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'
|
|
@@ -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
|
|
@@ -38,7 +40,9 @@ module SecureHeaders
|
|
|
38
40
|
SOURCE_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
|
|
39
41
|
|
|
40
42
|
ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER
|
|
43
|
+
CONFIG_KEY = :csp
|
|
41
44
|
end
|
|
45
|
+
|
|
42
46
|
include Constants
|
|
43
47
|
|
|
44
48
|
attr_reader :disable_fill_missing, :ssl_request
|
|
@@ -127,6 +131,7 @@ module SecureHeaders
|
|
|
127
131
|
|
|
128
132
|
@disable_fill_missing = !!@config.delete(:disable_fill_missing)
|
|
129
133
|
@enforce = !!@config.delete(:enforce)
|
|
134
|
+
@disable_img_src_data_uri = !!@config.delete(:disable_img_src_data_uri)
|
|
130
135
|
@tag_report_uri = !!@config.delete(:tag_report_uri)
|
|
131
136
|
@script_hashes = @config.delete(:script_hashes) || []
|
|
132
137
|
|
|
@@ -164,6 +169,21 @@ module SecureHeaders
|
|
|
164
169
|
end
|
|
165
170
|
end
|
|
166
171
|
|
|
172
|
+
def to_json
|
|
173
|
+
build_value
|
|
174
|
+
@config.to_json.gsub(/(\w+)_src/, "\\1-src")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.from_json(*json_configs)
|
|
178
|
+
json_configs.inject({}) do |combined_config, one_config|
|
|
179
|
+
one_config = one_config.gsub(/(\w+)-src/, "\\1_src")
|
|
180
|
+
config = JSON.parse(one_config, :symbolize_names => true)
|
|
181
|
+
combined_config.merge(config) do |_, lhs, rhs|
|
|
182
|
+
lhs | rhs
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
167
187
|
private
|
|
168
188
|
|
|
169
189
|
def add_script_hashes
|
|
@@ -205,8 +225,12 @@ module SecureHeaders
|
|
|
205
225
|
elsif %{self none}.include?(val)
|
|
206
226
|
"'#{val}'"
|
|
207
227
|
elsif val == 'nonce'
|
|
208
|
-
|
|
209
|
-
|
|
228
|
+
if supports_nonces?(@ua)
|
|
229
|
+
self.class.set_nonce(@controller, nonce)
|
|
230
|
+
["'nonce-#{nonce}'", "'unsafe-inline'"]
|
|
231
|
+
else
|
|
232
|
+
"'unsafe-inline'"
|
|
233
|
+
end
|
|
210
234
|
else
|
|
211
235
|
val
|
|
212
236
|
end
|
|
@@ -233,10 +257,11 @@ module SecureHeaders
|
|
|
233
257
|
|
|
234
258
|
def generic_directives
|
|
235
259
|
header_value = ''
|
|
260
|
+
data_uri = @disable_img_src_data_uri ? [] : ["data:"]
|
|
236
261
|
if @config[:img_src]
|
|
237
|
-
@config[:img_src] = @config[:img_src] +
|
|
262
|
+
@config[:img_src] = @config[:img_src] + data_uri unless @config[:img_src].include?('data:')
|
|
238
263
|
else
|
|
239
|
-
@config[:img_src] = @config[:default_src] +
|
|
264
|
+
@config[:img_src] = @config[:default_src] + data_uri
|
|
240
265
|
end
|
|
241
266
|
|
|
242
267
|
DIRECTIVES.each do |directive_name|
|
|
@@ -258,5 +283,10 @@ module SecureHeaders
|
|
|
258
283
|
def build_directive(key)
|
|
259
284
|
"#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
|
|
260
285
|
end
|
|
286
|
+
|
|
287
|
+
def supports_nonces?(user_agent)
|
|
288
|
+
parsed_ua = UserAgentParser.parse(user_agent)
|
|
289
|
+
["Chrome", "Opera", "Firefox"].include?(parsed_ua.family)
|
|
290
|
+
end
|
|
261
291
|
end
|
|
262
292
|
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
|
data/lib/secure_headers.rb
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
|
+
require "secure_headers/version"
|
|
2
|
+
require "secure_headers/header"
|
|
3
|
+
require "secure_headers/headers/public_key_pins"
|
|
4
|
+
require "secure_headers/headers/content_security_policy"
|
|
5
|
+
require "secure_headers/headers/x_frame_options"
|
|
6
|
+
require "secure_headers/headers/strict_transport_security"
|
|
7
|
+
require "secure_headers/headers/x_xss_protection"
|
|
8
|
+
require "secure_headers/headers/x_content_type_options"
|
|
9
|
+
require "secure_headers/headers/x_download_options"
|
|
10
|
+
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
11
|
+
require "secure_headers/railtie"
|
|
12
|
+
require "secure_headers/hash_helper"
|
|
13
|
+
require "secure_headers/view_helper"
|
|
14
|
+
|
|
1
15
|
module SecureHeaders
|
|
2
16
|
SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
|
|
3
17
|
HASHES_ENV_KEY = 'secure_headers.script_hashes'
|
|
4
18
|
|
|
19
|
+
ALL_HEADER_CLASSES = [
|
|
20
|
+
SecureHeaders::ContentSecurityPolicy,
|
|
21
|
+
SecureHeaders::StrictTransportSecurity,
|
|
22
|
+
SecureHeaders::PublicKeyPins,
|
|
23
|
+
SecureHeaders::XContentTypeOptions,
|
|
24
|
+
SecureHeaders::XDownloadOptions,
|
|
25
|
+
SecureHeaders::XFrameOptions,
|
|
26
|
+
SecureHeaders::XPermittedCrossDomainPolicies,
|
|
27
|
+
SecureHeaders::XXssProtection
|
|
28
|
+
]
|
|
29
|
+
|
|
5
30
|
module Configuration
|
|
6
31
|
class << self
|
|
7
32
|
attr_accessor :hsts, :x_frame_options, :x_content_type_options,
|
|
@@ -24,6 +49,27 @@ module SecureHeaders
|
|
|
24
49
|
include InstanceMethods
|
|
25
50
|
end
|
|
26
51
|
end
|
|
52
|
+
|
|
53
|
+
def header_hash(options = nil)
|
|
54
|
+
ALL_HEADER_CLASSES.inject({}) do |memo, klass|
|
|
55
|
+
config = if options.is_a?(Hash) && options[klass::Constants::CONFIG_KEY]
|
|
56
|
+
options[klass::Constants::CONFIG_KEY]
|
|
57
|
+
else
|
|
58
|
+
::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
unless klass == SecureHeaders::PublicKeyPins && !config.is_a?(Hash)
|
|
62
|
+
header = get_a_header(klass::Constants::CONFIG_KEY, klass, config)
|
|
63
|
+
memo[header.name] = header.value
|
|
64
|
+
end
|
|
65
|
+
memo
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def get_a_header(name, klass, options)
|
|
70
|
+
return if options == false
|
|
71
|
+
klass.new(options)
|
|
72
|
+
end
|
|
27
73
|
end
|
|
28
74
|
|
|
29
75
|
module ClassMethods
|
|
@@ -39,6 +85,9 @@ module SecureHeaders
|
|
|
39
85
|
end
|
|
40
86
|
|
|
41
87
|
def ensure_security_headers options = {}
|
|
88
|
+
if RUBY_VERSION == "1.8.7"
|
|
89
|
+
warn "[DEPRECATION] secure_headers ruby 1.8.7 support will dropped in the next release"
|
|
90
|
+
end
|
|
42
91
|
self.secure_headers_options = options
|
|
43
92
|
before_filter :prep_script_hash
|
|
44
93
|
before_filter :set_hsts_header
|
|
@@ -50,12 +99,6 @@ module SecureHeaders
|
|
|
50
99
|
before_filter :set_x_download_options_header
|
|
51
100
|
before_filter :set_x_permitted_cross_domain_policies_header
|
|
52
101
|
end
|
|
53
|
-
|
|
54
|
-
# we can't use ||= because I'm overloading false => disable, nil => default
|
|
55
|
-
# both of which trigger the conditional assignment
|
|
56
|
-
def options_for(type, options)
|
|
57
|
-
options.nil? ? ::SecureHeaders::Configuration.send(type) : options
|
|
58
|
-
end
|
|
59
102
|
end
|
|
60
103
|
|
|
61
104
|
module InstanceMethods
|
|
@@ -80,7 +123,7 @@ module SecureHeaders
|
|
|
80
123
|
end
|
|
81
124
|
|
|
82
125
|
config = self.class.secure_headers_options[:csp] if config.nil?
|
|
83
|
-
config =
|
|
126
|
+
config = secure_header_options_for :csp, config
|
|
84
127
|
|
|
85
128
|
return if config == false
|
|
86
129
|
|
|
@@ -140,7 +183,7 @@ module SecureHeaders
|
|
|
140
183
|
|
|
141
184
|
def set_hpkp_header(options=self.class.secure_headers_options[:hpkp])
|
|
142
185
|
return unless request.ssl?
|
|
143
|
-
config =
|
|
186
|
+
config = secure_header_options_for :hpkp, options
|
|
144
187
|
|
|
145
188
|
return if config == false || config.nil?
|
|
146
189
|
|
|
@@ -158,12 +201,16 @@ module SecureHeaders
|
|
|
158
201
|
|
|
159
202
|
private
|
|
160
203
|
|
|
204
|
+
# we can't use ||= because I'm overloading false => disable, nil => default
|
|
205
|
+
# both of which trigger the conditional assignment
|
|
206
|
+
def secure_header_options_for(type, options)
|
|
207
|
+
options.nil? ? ::SecureHeaders::Configuration.send(type) : options
|
|
208
|
+
end
|
|
209
|
+
|
|
161
210
|
def set_a_header(name, klass, options=nil)
|
|
162
|
-
options =
|
|
211
|
+
options = secure_header_options_for(name, options)
|
|
163
212
|
return if options == false
|
|
164
|
-
|
|
165
|
-
header = klass.new(options)
|
|
166
|
-
set_header(header)
|
|
213
|
+
set_header(SecureHeaders::get_a_header(name, klass, options))
|
|
167
214
|
end
|
|
168
215
|
|
|
169
216
|
def set_header(name_or_header, value=nil)
|
|
@@ -176,18 +223,3 @@ module SecureHeaders
|
|
|
176
223
|
end
|
|
177
224
|
end
|
|
178
225
|
end
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
require "secure_headers/version"
|
|
182
|
-
require "secure_headers/header"
|
|
183
|
-
require "secure_headers/headers/public_key_pins"
|
|
184
|
-
require "secure_headers/headers/content_security_policy"
|
|
185
|
-
require "secure_headers/headers/x_frame_options"
|
|
186
|
-
require "secure_headers/headers/strict_transport_security"
|
|
187
|
-
require "secure_headers/headers/x_xss_protection"
|
|
188
|
-
require "secure_headers/headers/x_content_type_options"
|
|
189
|
-
require "secure_headers/headers/x_download_options"
|
|
190
|
-
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
191
|
-
require "secure_headers/railtie"
|
|
192
|
-
require "secure_headers/hash_helper"
|
|
193
|
-
require "secure_headers/view_helper"
|
data/secure_headers.gemspec
CHANGED
|
@@ -19,5 +19,5 @@ Gem::Specification.new do |gem|
|
|
|
19
19
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
20
20
|
gem.require_paths = ["lib"]
|
|
21
21
|
gem.add_development_dependency "rake"
|
|
22
|
-
gem.
|
|
22
|
+
gem.add_dependency "user_agent_parser"
|
|
23
23
|
end
|
|
@@ -17,7 +17,8 @@ module SecureHeaders
|
|
|
17
17
|
FIREFOX_23 = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0"
|
|
18
18
|
CHROME = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4"
|
|
19
19
|
CHROME_25 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22"
|
|
20
|
-
|
|
20
|
+
SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A"
|
|
21
|
+
OPERA = "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16"
|
|
21
22
|
|
|
22
23
|
def request_for user_agent, request_uri=nil, options={:ssl => false}
|
|
23
24
|
double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') )
|
|
@@ -55,6 +56,23 @@ module SecureHeaders
|
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
it "exports a policy to JSON" do
|
|
60
|
+
policy = ContentSecurityPolicy.new(default_opts)
|
|
61
|
+
expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]})
|
|
62
|
+
expect(policy.to_json).to eq(expected)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "imports JSON to build a policy" do
|
|
66
|
+
json1 = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"]})
|
|
67
|
+
json2 = %({"style-src":["'unsafe-inline'"],"img-src":["https:","data:"]})
|
|
68
|
+
json3 = %({"style-src":["https:","about:"]})
|
|
69
|
+
config = ContentSecurityPolicy.from_json(json1, json2, json3)
|
|
70
|
+
policy = ContentSecurityPolicy.new(config.merge(:disable_fill_missing => true))
|
|
71
|
+
|
|
72
|
+
expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]})
|
|
73
|
+
expect(policy.to_json).to eq(expected)
|
|
74
|
+
end
|
|
75
|
+
|
|
58
76
|
context "when using hash sources" do
|
|
59
77
|
it "adds hashes and unsafe-inline to the script-src" do
|
|
60
78
|
policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123']))
|
|
@@ -155,6 +173,16 @@ module SecureHeaders
|
|
|
155
173
|
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
|
|
156
174
|
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
157
175
|
end
|
|
176
|
+
|
|
177
|
+
it "doesn't add a duplicate data uri if img-src specifies it already" do
|
|
178
|
+
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self data:', :disable_fill_missing => true}, :request => request_for(CHROME))
|
|
179
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "allows the user to disable img-src data: uris auto-whitelisting" do
|
|
183
|
+
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_img_src_data_uri => true, :disable_fill_missing => true}, :request => request_for(CHROME))
|
|
184
|
+
expect(csp.value).to eq("default-src 'self'; img-src 'self';")
|
|
185
|
+
end
|
|
158
186
|
end
|
|
159
187
|
|
|
160
188
|
it "fills in directives without values with default-src value" do
|
|
@@ -184,11 +212,33 @@ module SecureHeaders
|
|
|
184
212
|
end
|
|
185
213
|
|
|
186
214
|
context "when using a nonce" do
|
|
187
|
-
it "adds a nonce and unsafe-inline to the script-src value" do
|
|
215
|
+
it "adds a nonce and unsafe-inline to the script-src value when using chrome" do
|
|
188
216
|
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME))
|
|
189
217
|
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
190
218
|
end
|
|
191
219
|
|
|
220
|
+
it "adds a nonce and unsafe-inline to the script-src value when using firefox" do
|
|
221
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(FIREFOX))
|
|
222
|
+
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "adds a nonce and unsafe-inline to the script-src value when using opera" do
|
|
226
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(OPERA))
|
|
227
|
+
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it "does not add a nonce and unsafe-inline to the script-src value when using Safari" do
|
|
231
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(SAFARI))
|
|
232
|
+
expect(header.value).to include("script-src 'self' 'unsafe-inline'")
|
|
233
|
+
expect(header.value).not_to include("nonce")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it "does not add a nonce and unsafe-inline to the script-src value when using IE" do
|
|
237
|
+
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(IE))
|
|
238
|
+
expect(header.value).to include("script-src 'self' 'unsafe-inline'")
|
|
239
|
+
expect(header.value).not_to include("nonce")
|
|
240
|
+
end
|
|
241
|
+
|
|
192
242
|
it "adds a nonce and unsafe-inline to the style-src value" do
|
|
193
243
|
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME))
|
|
194
244
|
expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
@@ -159,6 +159,56 @@ describe SecureHeaders do
|
|
|
159
159
|
end
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
+
describe "SecureHeaders#header_hash" do
|
|
163
|
+
def expect_default_values(hash)
|
|
164
|
+
expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
|
|
165
|
+
expect(hash[XDO_HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE)
|
|
166
|
+
expect(hash[HSTS_HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE)
|
|
167
|
+
expect(hash[X_XSS_PROTECTION_HEADER_NAME]).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE)
|
|
168
|
+
expect(hash[X_CONTENT_TYPE_OPTIONS_HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
|
|
169
|
+
expect(hash[XPCDP_HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "produces a hash of headers given a hash as config" do
|
|
173
|
+
hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true})
|
|
174
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;")
|
|
175
|
+
expect_default_values(hash)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "produces a hash with a mix of config values, override values, and default values" do
|
|
179
|
+
::SecureHeaders::Configuration.configure do |config|
|
|
180
|
+
config.hsts = { :max_age => '123456'}
|
|
181
|
+
config.hpkp = {
|
|
182
|
+
:enforce => true,
|
|
183
|
+
:max_age => 1000000,
|
|
184
|
+
:include_subdomains => true,
|
|
185
|
+
:report_uri => '//example.com/uri-directive',
|
|
186
|
+
:pins => [
|
|
187
|
+
{:sha256 => 'abc'},
|
|
188
|
+
{:sha256 => '123'}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true})
|
|
194
|
+
::SecureHeaders::Configuration.configure do |config|
|
|
195
|
+
config.hsts = nil
|
|
196
|
+
config.hpkp = nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;")
|
|
200
|
+
expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
|
|
201
|
+
expect(hash[HSTS_HEADER_NAME]).to eq("max-age=123456")
|
|
202
|
+
expect(hash[HPKP_HEADER_NAME]).to eq(%{max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains})
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "produces a hash of headers with default config" do
|
|
206
|
+
hash = SecureHeaders::header_hash
|
|
207
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq(SecureHeaders::ContentSecurityPolicy::Constants::DEFAULT_CSP_HEADER)
|
|
208
|
+
expect_default_values(hash)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
162
212
|
describe "#set_x_frame_options_header" do
|
|
163
213
|
it "sets the X-Frame-Options header" do
|
|
164
214
|
should_assign_header(XFO_HEADER_NAME, SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
|
data/spec/spec_helper.rb
CHANGED
|
@@ -3,8 +3,11 @@ require 'rspec'
|
|
|
3
3
|
|
|
4
4
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
begin
|
|
7
|
+
require 'coveralls'
|
|
7
8
|
Coveralls.wear!
|
|
9
|
+
rescue LoadError
|
|
10
|
+
# damn you 1.8.7
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
include ::SecureHeaders::PublicKeyPins::Constants
|
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: 2.
|
|
4
|
+
version: 2.3.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: 2015-
|
|
11
|
+
date: 2015-09-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: user_agent_parser
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
27
41
|
description: Security related headers all in one gem.
|
|
28
42
|
email:
|
|
29
43
|
- neil.matatall@gmail.com
|
|
@@ -156,8 +170,7 @@ homepage: https://github.com/twitter/secureheaders
|
|
|
156
170
|
licenses:
|
|
157
171
|
- Apache Public License 2.0
|
|
158
172
|
metadata: {}
|
|
159
|
-
post_install_message:
|
|
160
|
-
|controller|. e.g. :enforce => lambda { |controller| some_expression }'
|
|
173
|
+
post_install_message:
|
|
161
174
|
rdoc_options: []
|
|
162
175
|
require_paths:
|
|
163
176
|
- lib
|
|
@@ -173,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
173
186
|
version: '0'
|
|
174
187
|
requirements: []
|
|
175
188
|
rubyforge_project:
|
|
176
|
-
rubygems_version: 2.
|
|
189
|
+
rubygems_version: 2.4.8
|
|
177
190
|
signing_key:
|
|
178
191
|
specification_version: 4
|
|
179
192
|
summary: Add easily configured security headers to responses including content-security-policy,
|