secure_headers 2.2.3 → 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/README.md +32 -2
- data/lib/secure_headers/headers/content_security_policy.rb +18 -0
- 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 +51 -20
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +17 -0
- data/spec/lib/secure_headers_spec.rb +50 -0
- metadata +2 -2
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/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,11 +415,41 @@ 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)
|
|
422
|
-
* J2EE Servlet >= 3.0 [
|
|
452
|
+
* J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines)
|
|
423
453
|
* ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki)
|
|
424
454
|
* Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security)
|
|
425
455
|
* Go - [secureheader](https://github.com/kr/secureheader)
|
|
@@ -2,6 +2,7 @@ require 'uri'
|
|
|
2
2
|
require 'base64'
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
require 'user_agent_parser'
|
|
5
|
+
require 'json'
|
|
5
6
|
|
|
6
7
|
module SecureHeaders
|
|
7
8
|
class ContentSecurityPolicyBuildError < StandardError; end
|
|
@@ -39,7 +40,9 @@ module SecureHeaders
|
|
|
39
40
|
SOURCE_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
|
|
40
41
|
|
|
41
42
|
ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER
|
|
43
|
+
CONFIG_KEY = :csp
|
|
42
44
|
end
|
|
45
|
+
|
|
43
46
|
include Constants
|
|
44
47
|
|
|
45
48
|
attr_reader :disable_fill_missing, :ssl_request
|
|
@@ -166,6 +169,21 @@ module SecureHeaders
|
|
|
166
169
|
end
|
|
167
170
|
end
|
|
168
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
|
+
|
|
169
187
|
private
|
|
170
188
|
|
|
171
189
|
def add_script_hashes
|
|
@@ -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
|
|
@@ -158,13 +207,10 @@ module SecureHeaders
|
|
|
158
207
|
options.nil? ? ::SecureHeaders::Configuration.send(type) : options
|
|
159
208
|
end
|
|
160
209
|
|
|
161
|
-
|
|
162
210
|
def set_a_header(name, klass, options=nil)
|
|
163
|
-
options = secure_header_options_for
|
|
211
|
+
options = secure_header_options_for(name, options)
|
|
164
212
|
return if options == false
|
|
165
|
-
|
|
166
|
-
header = klass.new(options)
|
|
167
|
-
set_header(header)
|
|
213
|
+
set_header(SecureHeaders::get_a_header(name, klass, options))
|
|
168
214
|
end
|
|
169
215
|
|
|
170
216
|
def set_header(name_or_header, value=nil)
|
|
@@ -177,18 +223,3 @@ module SecureHeaders
|
|
|
177
223
|
end
|
|
178
224
|
end
|
|
179
225
|
end
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
require "secure_headers/version"
|
|
183
|
-
require "secure_headers/header"
|
|
184
|
-
require "secure_headers/headers/public_key_pins"
|
|
185
|
-
require "secure_headers/headers/content_security_policy"
|
|
186
|
-
require "secure_headers/headers/x_frame_options"
|
|
187
|
-
require "secure_headers/headers/strict_transport_security"
|
|
188
|
-
require "secure_headers/headers/x_xss_protection"
|
|
189
|
-
require "secure_headers/headers/x_content_type_options"
|
|
190
|
-
require "secure_headers/headers/x_download_options"
|
|
191
|
-
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
192
|
-
require "secure_headers/railtie"
|
|
193
|
-
require "secure_headers/hash_helper"
|
|
194
|
-
require "secure_headers/view_helper"
|
|
@@ -56,6 +56,23 @@ module SecureHeaders
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
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
|
+
|
|
59
76
|
context "when using hash sources" do
|
|
60
77
|
it "adds hashes and unsafe-inline to the script-src" do
|
|
61
78
|
policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123']))
|
|
@@ -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)
|
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
|