secure_headers 1.0.0 → 2.0.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 (129) hide show
  1. data/.gitignore +4 -8
  2. data/.ruby-gemset +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +8 -3
  5. data/Gemfile +6 -8
  6. data/README.md +208 -134
  7. data/Rakefile +11 -116
  8. data/fixtures/rails_3_2_12/Gemfile +0 -8
  9. data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -1
  10. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -4
  11. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
  12. data/fixtures/rails_3_2_12/app/views/things/index.html.erb +1 -21
  13. data/fixtures/rails_3_2_12/config/application.rb +0 -54
  14. data/fixtures/rails_3_2_12/config/environments/test.rb +2 -2
  15. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +3 -1
  16. data/fixtures/rails_3_2_12/config/routes.rb +0 -57
  17. data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
  18. data/fixtures/rails_3_2_12/config.ru +3 -0
  19. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +57 -19
  20. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +18 -16
  21. data/fixtures/rails_3_2_12/spec/spec_helper.rb +1 -5
  22. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -9
  23. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
  24. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
  25. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
  26. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
  27. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -51
  28. data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +2 -2
  29. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
  30. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +17 -12
  31. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +17 -12
  32. data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -18
  33. data/fixtures/rails_4_1_8/Gemfile +5 -0
  34. data/fixtures/rails_4_1_8/README.rdoc +28 -0
  35. data/fixtures/rails_4_1_8/Rakefile +6 -0
  36. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
  37. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  38. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
  39. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
  40. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  41. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  42. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
  43. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
  44. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
  45. data/fixtures/rails_4_1_8/config/application.rb +15 -0
  46. data/fixtures/rails_4_1_8/config/boot.rb +4 -0
  47. data/fixtures/rails_4_1_8/config/environment.rb +5 -0
  48. data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
  49. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
  50. data/fixtures/rails_4_1_8/config/routes.rb +4 -0
  51. data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
  52. data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
  53. data/fixtures/rails_4_1_8/config.ru +4 -0
  54. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  55. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  56. data/fixtures/rails_4_1_8/log/.keep +0 -0
  57. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
  58. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
  59. data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
  60. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  61. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  62. data/lib/secure_headers/hash_helper.rb +7 -0
  63. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
  64. data/lib/secure_headers/headers/content_security_policy.rb +160 -161
  65. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  66. data/lib/secure_headers/headers/x_download_options.rb +39 -0
  67. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
  68. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  69. data/lib/secure_headers/padrino.rb +14 -0
  70. data/lib/secure_headers/railtie.rb +19 -24
  71. data/lib/secure_headers/version.rb +1 -1
  72. data/lib/secure_headers/view_helper.rb +68 -0
  73. data/lib/secure_headers.rb +70 -29
  74. data/lib/tasks/tasks.rake +48 -0
  75. data/secure_headers.gemspec +4 -5
  76. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
  77. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +127 -299
  78. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +21 -20
  79. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +12 -12
  80. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +32 -0
  81. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +12 -13
  82. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
  83. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +20 -19
  84. data/spec/lib/secure_headers_spec.rb +68 -83
  85. data/spec/spec_helper.rb +29 -18
  86. metadata +51 -66
  87. data/.rvmrc +0 -1
  88. data/Guardfile +0 -10
  89. data/HISTORY.md +0 -100
  90. data/app/controllers/content_security_policy_controller.rb +0 -53
  91. data/config/curl-ca-bundle.crt +0 -5420
  92. data/config/routes.rb +0 -3
  93. data/fixtures/rails_3_2_12/Guardfile +0 -14
  94. data/fixtures/rails_3_2_12/app/models/thing.rb +0 -3
  95. data/fixtures/rails_3_2_12/config/database.yml +0 -25
  96. data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
  97. data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
  98. data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
  99. data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
  100. data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
  101. data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
  102. data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
  103. data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
  104. data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
  105. data/fixtures/rails_3_2_12/db/schema.rb +0 -16
  106. data/fixtures/rails_3_2_12/db/seeds.rb +0 -7
  107. data/fixtures/rails_3_2_12_no_init/Guardfile +0 -14
  108. data/fixtures/rails_3_2_12_no_init/app/models/thing.rb +0 -3
  109. data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
  110. data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
  111. data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
  112. data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
  113. data/fixtures/rails_3_2_12_no_init/config/database.yml +0 -25
  114. data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
  115. data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
  116. data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
  117. data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
  118. data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
  119. data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
  120. data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
  121. data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
  122. data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
  123. data/fixtures/rails_3_2_12_no_init/db/schema.rb +0 -16
  124. data/fixtures/rails_3_2_12_no_init/db/seeds.rb +0 -7
  125. data/lib/secure_headers/headers/content_security_policy/browser_strategy.rb +0 -78
  126. data/lib/secure_headers/headers/content_security_policy/firefox_browser_strategy.rb +0 -72
  127. data/lib/secure_headers/headers/content_security_policy/ie_browser_strategy.rb +0 -6
  128. data/lib/secure_headers/headers/content_security_policy/standard_browser_strategy.rb +0 -22
  129. data/spec/controllers/content_security_policy_controller_spec.rb +0 -74
@@ -0,0 +1,68 @@
1
+ module SecureHeaders
2
+ class UnexpectedHashedScriptException < StandardError
3
+
4
+ end
5
+
6
+ module ViewHelpers
7
+ include SecureHeaders::HashHelper
8
+ SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes"
9
+
10
+ def nonced_style_tag(content = nil, &block)
11
+ nonced_tag(content, :style, block)
12
+ end
13
+
14
+ def nonced_javascript_tag(content = nil, &block)
15
+ nonced_tag(content, :script, block)
16
+ end
17
+
18
+ def hashed_javascript_tag(raise_error_on_unrecognized_hash = false, &block)
19
+ content = capture(&block)
20
+
21
+ if ['development', 'test'].include?(ENV["RAILS_ENV"])
22
+ hash_value = hash_source(content)
23
+ file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb')
24
+ script_hashes = controller.instance_variable_get(:@script_hashes)[file_path]
25
+ unless script_hashes && script_hashes.include?(hash_value)
26
+ message = unexpected_hash_error_message(file_path, hash_value, content)
27
+ if raise_error_on_unrecognized_hash
28
+ raise UnexpectedHashedScriptException.new(message)
29
+ else
30
+ puts message
31
+ request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value
32
+ end
33
+ end
34
+ end
35
+
36
+ content_tag :script, content
37
+ end
38
+
39
+ private
40
+
41
+ def nonced_tag(content, type, block)
42
+ content = if block
43
+ capture(&block)
44
+ else
45
+ content.html_safe # :'(
46
+ end
47
+
48
+ content_tag type, content, :nonce => @content_security_policy_nonce
49
+ end
50
+
51
+ def unexpected_hash_error_message(file_path, hash_value, content)
52
+ <<-EOF
53
+ \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
54
+ <script>#{content}</script>
55
+ *** This is fine in dev/test, but will raise exceptions in production. ***
56
+ *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
57
+ #{file_path}:
58
+ - #{hash_value}\n\n
59
+ EOF
60
+ end
61
+ end
62
+ end
63
+
64
+ module ActionView #:nodoc:
65
+ module Helpers #:nodoc:
66
+ include SecureHeaders::ViewHelpers
67
+ end
68
+ end
@@ -1,11 +1,18 @@
1
1
  module SecureHeaders
2
+ SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
3
+ HASHES_ENV_KEY = 'secure_headers.script_hashes'
4
+
2
5
  module Configuration
3
6
  class << self
4
7
  attr_accessor :hsts, :x_frame_options, :x_content_type_options,
5
- :x_xss_protection, :csp
8
+ :x_xss_protection, :csp, :x_download_options, :script_hashes,
9
+ :x_permitted_cross_domain_policies
6
10
 
7
11
  def configure &block
8
12
  instance_eval &block
13
+ if File.exists?(SCRIPT_HASH_CONFIG_FILE)
14
+ ::SecureHeaders::Configuration.script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE))
15
+ end
9
16
  end
10
17
  end
11
18
  end
@@ -33,11 +40,14 @@ module SecureHeaders
33
40
 
34
41
  def ensure_security_headers options = {}
35
42
  self.secure_headers_options = options
43
+ before_filter :prep_script_hash
36
44
  before_filter :set_hsts_header
37
45
  before_filter :set_x_frame_options_header
38
46
  before_filter :set_csp_header
39
47
  before_filter :set_x_xss_protection_header
40
48
  before_filter :set_x_content_type_options_header
49
+ before_filter :set_x_download_options_header
50
+ before_filter :set_x_permitted_cross_domain_policies_header
41
51
  end
42
52
 
43
53
  # we can't use ||= because I'm overloading false => disable, nil => default
@@ -48,35 +58,64 @@ module SecureHeaders
48
58
  end
49
59
 
50
60
  module InstanceMethods
51
- def brwsr
52
- @secure_headers_brwsr ||= Brwsr::Browser.new(:ua => request.env['HTTP_USER_AGENT'])
61
+ def set_security_headers(options = self.class.secure_headers_options)
62
+ set_csp_header(request, options[:csp])
63
+ set_hsts_header(options[:hsts])
64
+ set_x_frame_options_header(options[:x_frame_options])
65
+ set_x_xss_protection_header(options[:x_xss_protection])
66
+ set_x_content_type_options_header(options[:x_content_type_options])
67
+ set_x_download_options_header(options[:x_download_options])
68
+ set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies])
53
69
  end
54
70
 
55
- # backwards compatibility jank, to be removed in 1.0. Old API required a request
56
- # object when it didn't really need to.
57
71
  # set_csp_header - uses the request accessor and SecureHeader::Configuration settings
58
72
  # set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings
59
73
  # set_csp_header(+Hash+) - uses the request accessor and options from parameters
60
74
  # set_csp_header(+Rack::Request+, +Hash+)
61
- def set_csp_header(req = nil, options=nil)
62
- return if broken_implementation?(brwsr)
75
+ def set_csp_header(req = nil, config=nil)
76
+ if req.is_a?(Hash) || req.is_a?(FalseClass)
77
+ config = req
78
+ end
79
+
80
+ config = self.class.secure_headers_options[:csp] if config.nil?
81
+ config = self.class.options_for :csp, config
63
82
 
64
- if req.is_a?(Hash)
65
- options = req
66
- elsif req
67
- @secure_headers_brwsr = Brwsr::Browser.new(:ua => req.env['HTTP_USER_AGENT'])
83
+ return if config == false
84
+
85
+ if config && config[:script_hash_middleware]
86
+ ContentSecurityPolicy.add_to_env(request, self, config)
87
+ else
88
+ csp_header = ContentSecurityPolicy.new(config, :request => request, :controller => self)
89
+ set_header(csp_header)
68
90
  end
91
+ end
69
92
 
70
- options = self.class.secure_headers_options[:csp] if options.nil?
71
- options = self.class.options_for :csp, options
72
93
 
73
- return if options == false
94
+ def prep_script_hash
95
+ if ::SecureHeaders::Configuration.script_hashes
96
+ @script_hashes = ::SecureHeaders::Configuration.script_hashes.dup
97
+ ActiveSupport::Notifications.subscribe("render_partial.action_view") do |event_name, start_at, end_at, id, payload|
98
+ save_hash_for_later payload
99
+ end
100
+
101
+ ActiveSupport::Notifications.subscribe("render_template.action_view") do |event_name, start_at, end_at, id, payload|
102
+ save_hash_for_later payload
103
+ end
104
+ end
105
+ end
74
106
 
75
- csp_header = ContentSecurityPolicy.new(options, :request => request, :controller => self)
76
- set_header(csp_header)
77
- if options && options[:experimental] && options[:enforce]
78
- experimental_header = ContentSecurityPolicy.new(options, :experimental => true, :request => request, :controller => self)
79
- set_header(experimental_header)
107
+ def save_hash_for_later payload
108
+ matching_hashes = @script_hashes[payload[:identifier].gsub(Rails.root.to_s + "/", "")] || []
109
+
110
+ if payload[:layout]
111
+ # We're assuming an html.erb layout for now. Will need to handle mustache too, just not sure of the best way to do this
112
+ layout_hashes = @script_hashes[File.join("app", "views", payload[:layout]) + '.html.erb']
113
+
114
+ matching_hashes << layout_hashes if layout_hashes
115
+ end
116
+
117
+ if matching_hashes.any?
118
+ request.env[HASHES_ENV_KEY] = ((request.env[HASHES_ENV_KEY] || []) << matching_hashes).flatten
80
119
  end
81
120
  end
82
121
 
@@ -85,7 +124,6 @@ module SecureHeaders
85
124
  end
86
125
 
87
126
  def set_x_content_type_options_header(options=self.class.secure_headers_options[:x_content_type_options])
88
- return unless brwsr.ie? || brwsr.chrome?
89
127
  set_a_header(:x_content_type_options, XContentTypeOptions, options)
90
128
  end
91
129
 
@@ -98,6 +136,14 @@ module SecureHeaders
98
136
  set_a_header(:hsts, StrictTransportSecurity, options)
99
137
  end
100
138
 
139
+ def set_x_download_options_header(options=self.class.secure_headers_options[:x_download_options])
140
+ set_a_header(:x_download_options, XDownloadOptions, options)
141
+ end
142
+
143
+ def set_x_permitted_cross_domain_policies_header(options=self.class.secure_headers_options[:x_permitted_cross_domain_policies])
144
+ set_a_header(:x_permitted_cross_domain_policies, XPermittedCrossDomainPolicies, options)
145
+ end
146
+
101
147
  private
102
148
 
103
149
  def set_a_header(name, klass, options=nil)
@@ -116,10 +162,6 @@ module SecureHeaders
116
162
  response.headers[name_or_header] = value
117
163
  end
118
164
  end
119
-
120
- def broken_implementation?(browser)
121
- return browser.ios5? || (browser.safari? && browser.version == '5')
122
- end
123
165
  end
124
166
  end
125
167
 
@@ -127,13 +169,12 @@ end
127
169
  require "secure_headers/version"
128
170
  require "secure_headers/header"
129
171
  require "secure_headers/headers/content_security_policy"
130
- require "secure_headers/headers/content_security_policy/browser_strategy"
131
- require "secure_headers/headers/content_security_policy/firefox_browser_strategy"
132
- require "secure_headers/headers/content_security_policy/ie_browser_strategy"
133
- require "secure_headers/headers/content_security_policy/standard_browser_strategy"
134
172
  require "secure_headers/headers/x_frame_options"
135
173
  require "secure_headers/headers/strict_transport_security"
136
174
  require "secure_headers/headers/x_xss_protection"
137
175
  require "secure_headers/headers/x_content_type_options"
176
+ require "secure_headers/headers/x_download_options"
177
+ require "secure_headers/headers/x_permitted_cross_domain_policies"
138
178
  require "secure_headers/railtie"
139
- require "brwsr"
179
+ require "secure_headers/hash_helper"
180
+ require "secure_headers/view_helper"
@@ -0,0 +1,48 @@
1
+ INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
2
+ INLINE_HASH_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
3
+ SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
4
+
5
+ namespace :secure_headers do
6
+ include SecureHeaders::HashHelper
7
+
8
+ def is_erb?(filename)
9
+ filename =~ /\.erb\Z/
10
+ end
11
+
12
+ def generate_inline_script_hashes(filename)
13
+ file = File.read(filename)
14
+ hashes = []
15
+
16
+ [INLINE_SCRIPT_REGEX, INLINE_HASH_HELPER_REGEX].each do |regex|
17
+ file.gsub(regex) do # TODO don't use gsub
18
+ inline_script = Regexp.last_match.captures.last
19
+ if (filename =~ /\.mustache\Z/ && inline_script =~ /\{\{.*\}\}/) || (is_erb?(filename) && inline_script =~ /<%.*%>/)
20
+ puts "Looks like there's some dynamic content inside of a script tag :-/"
21
+ puts "That pretty much means the hash value will never match."
22
+ puts "Code: " + inline_script
23
+ puts "=" * 20
24
+ end
25
+
26
+ hashes << hash_source(inline_script)
27
+ end
28
+ end
29
+
30
+ hashes
31
+ end
32
+
33
+ task :generate_hashes do |t, args|
34
+ script_hashes = {}
35
+ Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename|
36
+ hashes = generate_inline_script_hashes(filename)
37
+ if hashes.any?
38
+ script_hashes[filename] = hashes
39
+ end
40
+ end
41
+
42
+ File.open(SCRIPT_HASH_CONFIG_FILE, 'w') do |file|
43
+ file.write(script_hashes.to_yaml)
44
+ end
45
+
46
+ puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SCRIPT_HASH_CONFIG_FILE}"
47
+ end
48
+ end
@@ -8,16 +8,15 @@ 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($/)
18
18
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
20
  gem.require_paths = ["lib"]
21
- gem.add_dependency "brwsr", ">= 1.1.1"
22
21
  gem.add_development_dependency "rake"
23
22
  end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'secure_headers/headers/content_security_policy/script_hash_middleware'
3
+
4
+ module SecureHeaders
5
+ describe ContentSecurityPolicy::ScriptHashMiddleware do
6
+
7
+ let(:app) { double(:call => [200, headers, '']) }
8
+ let(:env) { double }
9
+ let(:headers) { double }
10
+
11
+ let(:default_config) do
12
+ {
13
+ :disable_fill_missing => true,
14
+ :default_src => 'https://*',
15
+ :report_uri => '/csp_report',
16
+ :script_src => 'inline eval https://* data:',
17
+ :style_src => "inline https://* about:"
18
+ }
19
+ end
20
+
21
+ def should_assign_header name, value
22
+ expect(headers).to receive(:[]=).with(name, value)
23
+ end
24
+
25
+ def call_middleware(hashes = [])
26
+ options = {
27
+ :ua => USER_AGENTS[:chrome]
28
+ }
29
+ expect(env).to receive(:[]).with(HASHES_ENV_KEY).and_return(hashes)
30
+ expect(env).to receive(:[]).with(ENV_KEY).and_return(
31
+ :config => default_config,
32
+ :options => options
33
+ )
34
+ ContentSecurityPolicy::ScriptHashMiddleware.new(app).call(env)
35
+ end
36
+
37
+ it "adds hashes stored in env to the header" do
38
+ should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*'sha256-/)
39
+ call_middleware(['sha256-abc123'])
40
+ end
41
+
42
+ it "leaves things alone when no hashes are saved to env" do
43
+ should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*(?!'sha256-)/)
44
+ call_middleware()
45
+ end
46
+ end
47
+ end