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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +7 -1
  4. data/Gemfile +4 -4
  5. data/README.md +48 -34
  6. data/Rakefile +11 -0
  7. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -0
  8. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +5 -0
  9. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +5 -0
  10. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +5 -0
  11. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +5 -0
  12. data/fixtures/rails_4_1_8/Gemfile +5 -0
  13. data/fixtures/rails_4_1_8/README.rdoc +28 -0
  14. data/fixtures/rails_4_1_8/Rakefile +6 -0
  15. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
  16. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  17. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
  18. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
  19. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  20. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  21. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
  22. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
  23. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
  24. data/fixtures/rails_4_1_8/config/application.rb +15 -0
  25. data/fixtures/rails_4_1_8/config/boot.rb +4 -0
  26. data/fixtures/rails_4_1_8/config/environment.rb +5 -0
  27. data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
  28. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
  29. data/fixtures/rails_4_1_8/config/routes.rb +4 -0
  30. data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
  31. data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
  32. data/fixtures/rails_4_1_8/config.ru +4 -0
  33. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  34. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  35. data/fixtures/rails_4_1_8/log/.keep +0 -0
  36. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
  37. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
  38. data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
  39. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  40. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  41. data/lib/secure_headers/headers/content_security_policy.rb +8 -2
  42. data/lib/secure_headers/headers/public_key_pins.rb +95 -0
  43. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
  44. data/lib/secure_headers/railtie.rb +13 -0
  45. data/lib/secure_headers/version.rb +1 -1
  46. data/lib/secure_headers/view_helper.rb +1 -1
  47. data/lib/secure_headers.rb +22 -1
  48. data/secure_headers.gemspec +4 -4
  49. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +37 -0
  50. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +1 -1
  51. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
  52. data/spec/lib/secure_headers_spec.rb +69 -7
  53. data/spec/spec_helper.rb +2 -0
  54. metadata +43 -9
  55. data/Guardfile +0 -8
@@ -31,7 +31,13 @@ module SecureHeaders
31
31
  :reflected_xss
32
32
  ]
33
33
 
34
- ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
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 ALL_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
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
@@ -1,3 +1,3 @@
1
1
  module SecureHeaders
2
- VERSION = "2.0.0.pre"
2
+ VERSION = "2.1.0"
3
3
  end
@@ -62,7 +62,7 @@ module SecureHeaders
62
62
  end
63
63
 
64
64
  module ActionView #:nodoc:
65
- module Helpers #:nodoc:
65
+ class Base #:nodoc:
66
66
  include SecureHeaders::ViewHelpers
67
67
  end
68
68
  end
@@ -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"
@@ -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{Add easily configured browser headers to responses.}
12
- gem.summary = %q{Add easily configured browser headers to responses
13
- including content security policy, x-frame-options,
14
- strict-transport-security and more.}
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
@@ -24,7 +24,7 @@ module SecureHeaders
24
24
 
25
25
  it "doesn't accept anything besides noopen" do
26
26
  expect {
27
- XContentTypeOptions.new("open")
27
+ XDownloadOptions.new("open")
28
28
  }.to raise_error
29
29
  end
30
30
  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 = 6
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',