secure_headers 2.0.0.pre → 2.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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +7 -1
- data/Gemfile +4 -4
- data/README.md +48 -34
- data/Rakefile +11 -0
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +5 -0
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +5 -0
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +5 -0
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +5 -0
- data/fixtures/rails_4_1_8/Gemfile +5 -0
- data/fixtures/rails_4_1_8/README.rdoc +28 -0
- data/fixtures/rails_4_1_8/Rakefile +6 -0
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/models/.keep +0 -0
- data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
- data/fixtures/rails_4_1_8/config/application.rb +15 -0
- data/fixtures/rails_4_1_8/config/boot.rb +4 -0
- data/fixtures/rails_4_1_8/config/environment.rb +5 -0
- data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
- data/fixtures/rails_4_1_8/config/routes.rb +4 -0
- data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
- data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
- data/fixtures/rails_4_1_8/config.ru +4 -0
- data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
- data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
- data/fixtures/rails_4_1_8/log/.keep +0 -0
- data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
- data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
- data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
- data/lib/secure_headers/headers/content_security_policy.rb +8 -2
- data/lib/secure_headers/headers/public_key_pins.rb +95 -0
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
- data/lib/secure_headers/railtie.rb +13 -0
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +1 -1
- data/lib/secure_headers.rb +22 -1
- data/secure_headers.gemspec +4 -4
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +37 -0
- 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 +64 -0
- data/spec/lib/secure_headers_spec.rb +69 -7
- data/spec/spec_helper.rb +2 -0
- metadata +43 -9
- data/Guardfile +0 -8
|
File without changes
|
|
@@ -31,7 +31,13 @@ module SecureHeaders
|
|
|
31
31
|
:reflected_xss
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
OTHER = [
|
|
35
|
+
:report_uri
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
SOURCE_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
|
|
39
|
+
|
|
40
|
+
ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER
|
|
35
41
|
end
|
|
36
42
|
include Constants
|
|
37
43
|
|
|
@@ -102,7 +108,7 @@ module SecureHeaders
|
|
|
102
108
|
@config = config.inject({}) do |hash, (key, value)|
|
|
103
109
|
config_val = value.respond_to?(:call) ? value.call : value
|
|
104
110
|
|
|
105
|
-
if
|
|
111
|
+
if SOURCE_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
|
|
106
112
|
config_val = config_val.split if config_val.is_a? String
|
|
107
113
|
if config_val.is_a?(Array)
|
|
108
114
|
config_val = config_val.map do |val|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class PublicKeyPinsBuildError < StandardError; end
|
|
3
|
+
class PublicKeyPins < Header
|
|
4
|
+
module Constants
|
|
5
|
+
HPKP_HEADER_NAME = "Public-Key-Pins"
|
|
6
|
+
ENV_KEY = 'secure_headers.public_key_pins'
|
|
7
|
+
HASH_ALGORITHMS = [:sha256]
|
|
8
|
+
DIRECTIVES = [:max_age]
|
|
9
|
+
end
|
|
10
|
+
class << self
|
|
11
|
+
def symbol_to_hyphen_case sym
|
|
12
|
+
sym.to_s.gsub('_', '-')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
include Constants
|
|
16
|
+
|
|
17
|
+
def initialize(config=nil)
|
|
18
|
+
@config = validate_config(config)
|
|
19
|
+
|
|
20
|
+
@pins = @config.fetch(:pins, nil)
|
|
21
|
+
@report_uri = @config.fetch(:report_uri, nil)
|
|
22
|
+
@app_name = @config.fetch(:app_name, nil)
|
|
23
|
+
@enforce = !!@config.fetch(:enforce, nil)
|
|
24
|
+
@include_subdomains = !!@config.fetch(:include_subdomains, nil)
|
|
25
|
+
@tag_report_uri = !!@config.fetch(:tag_report_uri, nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def name
|
|
29
|
+
base = HPKP_HEADER_NAME
|
|
30
|
+
if !@enforce
|
|
31
|
+
base += "-Report-Only"
|
|
32
|
+
end
|
|
33
|
+
base
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def value
|
|
37
|
+
header_value = [
|
|
38
|
+
generic_directives,
|
|
39
|
+
pin_directives,
|
|
40
|
+
report_uri_directive,
|
|
41
|
+
subdomain_directive
|
|
42
|
+
].compact.join('; ').strip
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_config(config)
|
|
46
|
+
raise PublicKeyPinsBuildError.new("config must be a hash.") unless config.is_a? Hash
|
|
47
|
+
|
|
48
|
+
if !config[:max_age]
|
|
49
|
+
raise PublicKeyPinsBuildError.new("max-age is a required directive.")
|
|
50
|
+
elsif config[:max_age].to_s !~ /\A\d+\z/
|
|
51
|
+
raise PublicKeyPinsBuildError.new("max-age must be a number.
|
|
52
|
+
#{config[:max_age]} was supplied.")
|
|
53
|
+
elsif config[:pins] && config[:pins].length < 2
|
|
54
|
+
raise PublicKeyPinsBuildError.new("A minimum of 2 pins are required.")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
config
|
|
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 generic_directives
|
|
70
|
+
DIRECTIVES.collect do |directive_name|
|
|
71
|
+
build_directive(directive_name) if @config[directive_name]
|
|
72
|
+
end.join('; ')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_directive(key)
|
|
76
|
+
"#{self.class.symbol_to_hyphen_case(key)}=#{@config[key]}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def report_uri_directive
|
|
80
|
+
return nil if @report_uri.nil?
|
|
81
|
+
|
|
82
|
+
if @tag_report_uri
|
|
83
|
+
@report_uri = "#{@report_uri}?enforce=#{@enforce}"
|
|
84
|
+
@report_uri += "&app_name=#{@app_name}" if @app_name
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
"report-uri=\"#{@report_uri}\""
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def subdomain_directive
|
|
92
|
+
@include_subdomains ? 'includeSubDomains' : nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class XPCDPBuildError < StandardError; end
|
|
3
|
+
class XPermittedCrossDomainPolicies < Header
|
|
4
|
+
module Constants
|
|
5
|
+
XPCDP_HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
|
|
6
|
+
DEFAULT_VALUE = 'none'
|
|
7
|
+
VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
|
|
8
|
+
end
|
|
9
|
+
include Constants
|
|
10
|
+
|
|
11
|
+
def initialize(config = nil)
|
|
12
|
+
@config = config
|
|
13
|
+
validate_config unless @config.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def name
|
|
17
|
+
XPCDP_HEADER_NAME
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def value
|
|
21
|
+
case @config
|
|
22
|
+
when NilClass
|
|
23
|
+
DEFAULT_VALUE
|
|
24
|
+
when String
|
|
25
|
+
@config
|
|
26
|
+
else
|
|
27
|
+
@config[:value]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def validate_config
|
|
34
|
+
value = @config.is_a?(Hash) ? @config[:value] : @config
|
|
35
|
+
unless VALID_POLICIES.include?(value.downcase)
|
|
36
|
+
raise XPCDPBuildError.new("Value can only be one of #{VALID_POLICIES.join(', ')}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -3,9 +3,22 @@ if defined?(Rails::Railtie)
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
class Railtie < Rails::Engine
|
|
5
5
|
isolate_namespace ::SecureHeaders if defined? isolate_namespace # rails 3.0
|
|
6
|
+
conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', 'X-Content-Type-Options',
|
|
7
|
+
'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
|
|
8
|
+
'X-Content-Type-Options', 'Strict-Transport-Security',
|
|
9
|
+
'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
|
|
10
|
+
'X-Permitted-Cross-Domain-Policies','Public-Key-Pins','Public-Key-Pins-Report-Only']
|
|
11
|
+
|
|
6
12
|
initializer "secure_headers.action_controller" do
|
|
7
13
|
ActiveSupport.on_load(:action_controller) do
|
|
8
14
|
include ::SecureHeaders
|
|
15
|
+
|
|
16
|
+
unless Rails.application.config.action_dispatch.default_headers.nil?
|
|
17
|
+
conflicting_headers.each do |header|
|
|
18
|
+
Rails.application.config.action_dispatch.default_headers.delete(header)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
9
22
|
end
|
|
10
23
|
end
|
|
11
24
|
end
|
data/lib/secure_headers.rb
CHANGED
|
@@ -5,7 +5,8 @@ module SecureHeaders
|
|
|
5
5
|
module Configuration
|
|
6
6
|
class << self
|
|
7
7
|
attr_accessor :hsts, :x_frame_options, :x_content_type_options,
|
|
8
|
-
:x_xss_protection, :csp, :x_download_options, :script_hashes
|
|
8
|
+
:x_xss_protection, :csp, :x_download_options, :script_hashes,
|
|
9
|
+
:x_permitted_cross_domain_policies, :hpkp
|
|
9
10
|
|
|
10
11
|
def configure &block
|
|
11
12
|
instance_eval &block
|
|
@@ -41,11 +42,13 @@ module SecureHeaders
|
|
|
41
42
|
self.secure_headers_options = options
|
|
42
43
|
before_filter :prep_script_hash
|
|
43
44
|
before_filter :set_hsts_header
|
|
45
|
+
before_filter :set_hpkp_header
|
|
44
46
|
before_filter :set_x_frame_options_header
|
|
45
47
|
before_filter :set_csp_header
|
|
46
48
|
before_filter :set_x_xss_protection_header
|
|
47
49
|
before_filter :set_x_content_type_options_header
|
|
48
50
|
before_filter :set_x_download_options_header
|
|
51
|
+
before_filter :set_x_permitted_cross_domain_policies_header
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
# we can't use ||= because I'm overloading false => disable, nil => default
|
|
@@ -59,10 +62,12 @@ module SecureHeaders
|
|
|
59
62
|
def set_security_headers(options = self.class.secure_headers_options)
|
|
60
63
|
set_csp_header(request, options[:csp])
|
|
61
64
|
set_hsts_header(options[:hsts])
|
|
65
|
+
set_hpkp_header(options[:hpkp])
|
|
62
66
|
set_x_frame_options_header(options[:x_frame_options])
|
|
63
67
|
set_x_xss_protection_header(options[:x_xss_protection])
|
|
64
68
|
set_x_content_type_options_header(options[:x_content_type_options])
|
|
65
69
|
set_x_download_options_header(options[:x_download_options])
|
|
70
|
+
set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies])
|
|
66
71
|
end
|
|
67
72
|
|
|
68
73
|
# set_csp_header - uses the request accessor and SecureHeader::Configuration settings
|
|
@@ -133,10 +138,24 @@ module SecureHeaders
|
|
|
133
138
|
set_a_header(:hsts, StrictTransportSecurity, options)
|
|
134
139
|
end
|
|
135
140
|
|
|
141
|
+
def set_hpkp_header(options=self.class.secure_headers_options[:hpkp])
|
|
142
|
+
return unless request.ssl?
|
|
143
|
+
config = self.class.options_for :hpkp, options
|
|
144
|
+
|
|
145
|
+
return if config == false || config.nil?
|
|
146
|
+
|
|
147
|
+
hpkp_header = PublicKeyPins.new(config)
|
|
148
|
+
set_header(hpkp_header)
|
|
149
|
+
end
|
|
150
|
+
|
|
136
151
|
def set_x_download_options_header(options=self.class.secure_headers_options[:x_download_options])
|
|
137
152
|
set_a_header(:x_download_options, XDownloadOptions, options)
|
|
138
153
|
end
|
|
139
154
|
|
|
155
|
+
def set_x_permitted_cross_domain_policies_header(options=self.class.secure_headers_options[:x_permitted_cross_domain_policies])
|
|
156
|
+
set_a_header(:x_permitted_cross_domain_policies, XPermittedCrossDomainPolicies, options)
|
|
157
|
+
end
|
|
158
|
+
|
|
140
159
|
private
|
|
141
160
|
|
|
142
161
|
def set_a_header(name, klass, options=nil)
|
|
@@ -161,12 +180,14 @@ end
|
|
|
161
180
|
|
|
162
181
|
require "secure_headers/version"
|
|
163
182
|
require "secure_headers/header"
|
|
183
|
+
require "secure_headers/headers/public_key_pins"
|
|
164
184
|
require "secure_headers/headers/content_security_policy"
|
|
165
185
|
require "secure_headers/headers/x_frame_options"
|
|
166
186
|
require "secure_headers/headers/strict_transport_security"
|
|
167
187
|
require "secure_headers/headers/x_xss_protection"
|
|
168
188
|
require "secure_headers/headers/x_content_type_options"
|
|
169
189
|
require "secure_headers/headers/x_download_options"
|
|
190
|
+
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
170
191
|
require "secure_headers/railtie"
|
|
171
192
|
require "secure_headers/hash_helper"
|
|
172
193
|
require "secure_headers/view_helper"
|
data/secure_headers.gemspec
CHANGED
|
@@ -8,10 +8,10 @@ Gem::Specification.new do |gem|
|
|
|
8
8
|
gem.version = SecureHeaders::VERSION
|
|
9
9
|
gem.authors = ["Neil Matatall"]
|
|
10
10
|
gem.email = ["neil.matatall@gmail.com"]
|
|
11
|
-
gem.description = %q{
|
|
12
|
-
gem.summary = %q{Add easily configured
|
|
13
|
-
including content
|
|
14
|
-
strict-transport-security
|
|
11
|
+
gem.description = %q{Security related headers all in one gem.}
|
|
12
|
+
gem.summary = %q{Add easily configured security headers to responses
|
|
13
|
+
including content-security-policy, x-frame-options,
|
|
14
|
+
strict-transport-security, etc.}
|
|
15
15
|
gem.homepage = "https://github.com/twitter/secureheaders"
|
|
16
16
|
gem.license = "Apache Public License 2.0"
|
|
17
17
|
gem.files = `git ls-files`.split($/)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe PublicKeyPins do
|
|
5
|
+
specify{ expect(PublicKeyPins.new(:max_age => 1234).name).to eq("Public-Key-Pins-Report-Only") }
|
|
6
|
+
specify{ expect(PublicKeyPins.new(:max_age => 1234, :enforce => true).name).to eq("Public-Key-Pins") }
|
|
7
|
+
|
|
8
|
+
specify { expect(PublicKeyPins.new({:max_age => 1234}).value).to eq("max-age=1234")}
|
|
9
|
+
specify { expect(PublicKeyPins.new(:max_age => 1234).value).to eq("max-age=1234")}
|
|
10
|
+
specify {
|
|
11
|
+
config = {:max_age => 1234, :pins => [{:sha256 => 'base64encodedpin1'}, {:sha256 => 'base64encodedpin2'}]}
|
|
12
|
+
header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\""
|
|
13
|
+
expect(PublicKeyPins.new(config).value).to eq(header_value)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
context "with an invalid configuration" do
|
|
17
|
+
it "raises an exception when max-age is not provided" do
|
|
18
|
+
expect {
|
|
19
|
+
PublicKeyPins.new(:foo => 'bar')
|
|
20
|
+
}.to raise_error(PublicKeyPinsBuildError)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "raises an exception with an invalid max-age" do
|
|
24
|
+
expect {
|
|
25
|
+
PublicKeyPins.new(:max_age => 'abc123')
|
|
26
|
+
}.to raise_error(PublicKeyPinsBuildError)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'raises an exception with less than 2 pins' do
|
|
30
|
+
expect {
|
|
31
|
+
config = {:max_age => 1234, :pins => [{:sha256 => 'base64encodedpin'}]}
|
|
32
|
+
PublicKeyPins.new(config)
|
|
33
|
+
}.to raise_error(PublicKeyPinsBuildError)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
describe XPermittedCrossDomainPolicies do
|
|
3
|
+
specify { expect(XPermittedCrossDomainPolicies.new.name).to eq(XPermittedCrossDomainPolicies::Constants::XPCDP_HEADER_NAME)}
|
|
4
|
+
specify { expect(XPermittedCrossDomainPolicies.new.value).to eq("none")}
|
|
5
|
+
specify { expect(XPermittedCrossDomainPolicies.new('master-only').value).to eq('master-only')}
|
|
6
|
+
specify { expect(XPermittedCrossDomainPolicies.new(:value => 'master-only').value).to eq('master-only') }
|
|
7
|
+
|
|
8
|
+
context "valid configuration values" do
|
|
9
|
+
it "accepts 'all'" do
|
|
10
|
+
expect {
|
|
11
|
+
XPermittedCrossDomainPolicies.new("all")
|
|
12
|
+
}.not_to raise_error
|
|
13
|
+
|
|
14
|
+
expect {
|
|
15
|
+
XPermittedCrossDomainPolicies.new(:value => "all")
|
|
16
|
+
}.not_to raise_error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "accepts 'by-ftp-filename'" do
|
|
20
|
+
expect {
|
|
21
|
+
XPermittedCrossDomainPolicies.new("by-ftp-filename")
|
|
22
|
+
}.not_to raise_error
|
|
23
|
+
|
|
24
|
+
expect {
|
|
25
|
+
XPermittedCrossDomainPolicies.new(:value => "by-ftp-filename")
|
|
26
|
+
}.not_to raise_error
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "accepts 'by-content-type'" do
|
|
30
|
+
expect {
|
|
31
|
+
XPermittedCrossDomainPolicies.new("by-content-type")
|
|
32
|
+
}.not_to raise_error
|
|
33
|
+
|
|
34
|
+
expect {
|
|
35
|
+
XPermittedCrossDomainPolicies.new(:value => "by-content-type")
|
|
36
|
+
}.not_to raise_error
|
|
37
|
+
end
|
|
38
|
+
it "accepts 'master-only'" do
|
|
39
|
+
expect {
|
|
40
|
+
XPermittedCrossDomainPolicies.new("master-only")
|
|
41
|
+
}.not_to raise_error
|
|
42
|
+
|
|
43
|
+
expect {
|
|
44
|
+
XPermittedCrossDomainPolicies.new(:value => "master-only")
|
|
45
|
+
}.not_to raise_error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "accepts nil" do
|
|
49
|
+
expect {
|
|
50
|
+
XPermittedCrossDomainPolicies.new
|
|
51
|
+
}.not_to raise_error
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context 'invlaid configuration values' do
|
|
56
|
+
|
|
57
|
+
it "doesn't accept invalid values" do
|
|
58
|
+
expect {
|
|
59
|
+
XPermittedCrossDomainPolicies.new("open")
|
|
60
|
+
}.to raise_error
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -18,34 +18,32 @@ describe SecureHeaders do
|
|
|
18
18
|
allow(subject).to receive(:request).and_return(request)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
ALL_HEADERS = Hash[[:hsts, :csp, :x_frame_options, :x_content_type_options, :x_xss_protection].map{|header| [header, false]}]
|
|
22
|
-
|
|
23
21
|
def stub_user_agent val
|
|
24
22
|
allow(request).to receive_message_chain(:env, :[]).and_return(val)
|
|
25
23
|
end
|
|
26
24
|
|
|
27
|
-
def options_for header
|
|
28
|
-
ALL_HEADERS.reject{|k,v| k == header}
|
|
29
|
-
end
|
|
30
|
-
|
|
31
25
|
def reset_config
|
|
32
26
|
::SecureHeaders::Configuration.configure do |config|
|
|
27
|
+
config.hpkp = nil
|
|
33
28
|
config.hsts = nil
|
|
34
29
|
config.x_frame_options = nil
|
|
35
30
|
config.x_content_type_options = nil
|
|
36
31
|
config.x_xss_protection = nil
|
|
37
32
|
config.csp = nil
|
|
38
33
|
config.x_download_options = nil
|
|
34
|
+
config.x_permitted_cross_domain_policies = nil
|
|
39
35
|
end
|
|
40
36
|
end
|
|
41
37
|
|
|
42
38
|
def set_security_headers(subject)
|
|
43
39
|
subject.set_csp_header
|
|
40
|
+
subject.set_hpkp_header
|
|
44
41
|
subject.set_hsts_header
|
|
45
42
|
subject.set_x_frame_options_header
|
|
46
43
|
subject.set_x_content_type_options_header
|
|
47
44
|
subject.set_x_xss_protection_header
|
|
48
45
|
subject.set_x_download_options_header
|
|
46
|
+
subject.set_x_permitted_cross_domain_policies_header
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
describe "#set_header" do
|
|
@@ -64,14 +62,16 @@ describe SecureHeaders do
|
|
|
64
62
|
USER_AGENTS.each do |name, useragent|
|
|
65
63
|
it "sets all default headers for #{name} (smoke test)" do
|
|
66
64
|
stub_user_agent(useragent)
|
|
67
|
-
number_of_headers =
|
|
65
|
+
number_of_headers = 7
|
|
68
66
|
expect(subject).to receive(:set_header).exactly(number_of_headers).times # a request for a given header
|
|
69
67
|
subject.set_csp_header
|
|
70
68
|
subject.set_x_frame_options_header
|
|
71
69
|
subject.set_hsts_header
|
|
70
|
+
subject.set_hpkp_header
|
|
72
71
|
subject.set_x_xss_protection_header
|
|
73
72
|
subject.set_x_content_type_options_header
|
|
74
73
|
subject.set_x_download_options_header
|
|
74
|
+
subject.set_x_permitted_cross_domain_policies_header
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -96,6 +96,11 @@ describe SecureHeaders do
|
|
|
96
96
|
subject.set_x_frame_options_header(false)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
it "does not set the X-Permitted-Cross-Domain-Policies header if disabled" do
|
|
100
|
+
should_not_assign_header(XPCDP_HEADER_NAME)
|
|
101
|
+
subject.set_x_permitted_cross_domain_policies_header(false)
|
|
102
|
+
end
|
|
103
|
+
|
|
99
104
|
it "does not set the HSTS header if disabled" do
|
|
100
105
|
should_not_assign_header(HSTS_HEADER_NAME)
|
|
101
106
|
subject.set_hsts_header(false)
|
|
@@ -107,6 +112,17 @@ describe SecureHeaders do
|
|
|
107
112
|
subject.set_hsts_header({:include_subdomains => true})
|
|
108
113
|
end
|
|
109
114
|
|
|
115
|
+
it "does not set the HPKP header if disabled" do
|
|
116
|
+
should_not_assign_header(HPKP_HEADER_NAME)
|
|
117
|
+
subject.set_hpkp_header
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "does not set the HPKP header if request is over HTTP" do
|
|
121
|
+
allow(subject).to receive_message_chain(:request, :ssl?).and_return(false)
|
|
122
|
+
should_not_assign_header(HPKP_HEADER_NAME)
|
|
123
|
+
subject.set_hpkp_header(:max_age => 1234)
|
|
124
|
+
end
|
|
125
|
+
|
|
110
126
|
it "does not set the CSP header if disabled" do
|
|
111
127
|
stub_user_agent(USER_AGENTS[:chrome])
|
|
112
128
|
should_not_assign_header(HEADER_NAME)
|
|
@@ -128,11 +144,13 @@ describe SecureHeaders do
|
|
|
128
144
|
it "does not set any headers when disabled" do
|
|
129
145
|
::SecureHeaders::Configuration.configure do |config|
|
|
130
146
|
config.hsts = false
|
|
147
|
+
config.hpkp = false
|
|
131
148
|
config.x_frame_options = false
|
|
132
149
|
config.x_content_type_options = false
|
|
133
150
|
config.x_xss_protection = false
|
|
134
151
|
config.csp = false
|
|
135
152
|
config.x_download_options = false
|
|
153
|
+
config.x_permitted_cross_domain_policies = false
|
|
136
154
|
end
|
|
137
155
|
expect(subject).not_to receive(:set_header)
|
|
138
156
|
set_security_headers(subject)
|
|
@@ -187,6 +205,38 @@ describe SecureHeaders do
|
|
|
187
205
|
end
|
|
188
206
|
end
|
|
189
207
|
|
|
208
|
+
describe "#set_public_key_pins" do
|
|
209
|
+
it "sets the Public-Key-Pins header" do
|
|
210
|
+
should_assign_header(HPKP_HEADER_NAME + "-Report-Only", "max-age=1234")
|
|
211
|
+
subject.set_hpkp_header(:max_age => 1234)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "allows you to enforce public key pinning" do
|
|
215
|
+
should_assign_header(HPKP_HEADER_NAME, "max-age=1234")
|
|
216
|
+
subject.set_hpkp_header(:max_age => 1234, :enforce => true)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "allows you to specific a custom max-age value" do
|
|
220
|
+
should_assign_header(HPKP_HEADER_NAME + "-Report-Only", 'max-age=1234')
|
|
221
|
+
subject.set_hpkp_header(:max_age => 1234)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "allows you to specify includeSubdomains" do
|
|
225
|
+
should_assign_header(HPKP_HEADER_NAME, "max-age=1234; includeSubDomains")
|
|
226
|
+
subject.set_hpkp_header(:max_age => 1234, :include_subdomains => true, :enforce => true)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "allows you to specify a report-uri" do
|
|
230
|
+
should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com\"")
|
|
231
|
+
subject.set_hpkp_header(:max_age => 1234, :report_uri => "https://foobar.com", :enforce => true)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "allows you to specify a report-uri with app_name" do
|
|
235
|
+
should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com?enforce=true&app_name=my_app\"")
|
|
236
|
+
subject.set_hpkp_header(:max_age => 1234, :report_uri => "https://foobar.com", :app_name => "my_app", :tag_report_uri => true, :enforce => true)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
190
240
|
describe "#set_x_xss_protection" do
|
|
191
241
|
it "sets the X-XSS-Protection header" do
|
|
192
242
|
should_assign_header(X_XSS_PROTECTION_HEADER_NAME, SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE)
|
|
@@ -249,4 +299,16 @@ describe SecureHeaders do
|
|
|
249
299
|
end
|
|
250
300
|
end
|
|
251
301
|
end
|
|
302
|
+
|
|
303
|
+
describe "#set_x_permitted_cross_domain_policies_header" do
|
|
304
|
+
it "sets the X-Permitted-Cross-Domain-Policies header" do
|
|
305
|
+
should_assign_header(XPCDP_HEADER_NAME, SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE)
|
|
306
|
+
subject.set_x_permitted_cross_domain_policies_header
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it "allows a custom X-Permitted-Cross-Domain-Policies header" do
|
|
310
|
+
should_assign_header(XPCDP_HEADER_NAME, "master-only")
|
|
311
|
+
subject.set_x_permitted_cross_domain_policies_header(:value => 'master-only')
|
|
312
|
+
end
|
|
313
|
+
end
|
|
252
314
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -7,12 +7,14 @@ if defined?(Coveralls)
|
|
|
7
7
|
Coveralls.wear!
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
include ::SecureHeaders::PublicKeyPins::Constants
|
|
10
11
|
include ::SecureHeaders::StrictTransportSecurity::Constants
|
|
11
12
|
include ::SecureHeaders::ContentSecurityPolicy::Constants
|
|
12
13
|
include ::SecureHeaders::XFrameOptions::Constants
|
|
13
14
|
include ::SecureHeaders::XXssProtection::Constants
|
|
14
15
|
include ::SecureHeaders::XContentTypeOptions::Constants
|
|
15
16
|
include ::SecureHeaders::XDownloadOptions::Constants
|
|
17
|
+
include ::SecureHeaders::XPermittedCrossDomainPolicies::Constants
|
|
16
18
|
|
|
17
19
|
USER_AGENTS = {
|
|
18
20
|
:firefox => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
|