secure_headers 1.4.1 → 2.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -8
  3. data/Gemfile +2 -2
  4. data/Guardfile +8 -0
  5. data/README.md +102 -48
  6. data/Rakefile +0 -116
  7. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -1
  8. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
  9. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -1
  10. data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
  11. data/fixtures/rails_3_2_12/config.ru +3 -0
  12. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +50 -18
  13. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +1 -1
  14. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
  15. data/lib/secure_headers/hash_helper.rb +7 -0
  16. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
  17. data/lib/secure_headers/headers/content_security_policy.rb +141 -137
  18. data/lib/secure_headers/railtie.rb +0 -22
  19. data/lib/secure_headers/version.rb +1 -1
  20. data/lib/secure_headers/view_helper.rb +68 -0
  21. data/lib/secure_headers.rb +51 -17
  22. data/lib/tasks/tasks.rake +48 -0
  23. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
  24. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +83 -208
  25. data/spec/lib/secure_headers_spec.rb +16 -62
  26. data/spec/spec_helper.rb +25 -1
  27. metadata +22 -24
  28. data/HISTORY.md +0 -162
  29. data/app/controllers/content_security_policy_controller.rb +0 -76
  30. data/config/curl-ca-bundle.crt +0 -5420
  31. data/config/routes.rb +0 -3
  32. data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
@@ -1,93 +1,154 @@
1
1
  require 'uri'
2
2
  require 'base64'
3
+ require 'securerandom'
3
4
 
4
5
  module SecureHeaders
5
6
  class ContentSecurityPolicyBuildError < StandardError; end
6
7
  class ContentSecurityPolicy < Header
7
8
  module Constants
8
- DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https://* about: javascript:; img-src data:"
9
- STANDARD_HEADER_NAME = "Content-Security-Policy"
10
- FF_CSP_ENDPOINT = "/content_security_policy/forward_report"
11
- DIRECTIVES = [:default_src, :script_src, :frame_src, :style_src, :img_src, :media_src, :font_src, :object_src, :connect_src]
12
- META = [:disable_chrome_extension, :disable_fill_missing, :forward_endpoint]
9
+ DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:"
10
+ HEADER_NAME = "Content-Security-Policy"
11
+ ENV_KEY = 'secure_headers.content_security_policy'
12
+ DIRECTIVES = [
13
+ :default_src,
14
+ :connect_src,
15
+ :font_src,
16
+ :frame_src,
17
+ :img_src,
18
+ :media_src,
19
+ :object_src,
20
+ :script_src,
21
+ :style_src
22
+ ]
23
+
24
+ NON_DEFAULT_SOURCES = [
25
+ :base_uri,
26
+ :child_src,
27
+ :form_action,
28
+ :frame_ancestors,
29
+ :plugin_types,
30
+ :referrer,
31
+ :reflected_xss
32
+ ]
33
+
34
+ ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
13
35
  end
14
36
  include Constants
15
37
 
16
- attr_accessor *META
17
- attr_reader :browser, :ssl_request, :report_uri, :request_uri, :experimental
18
-
19
- alias :disable_chrome_extension? :disable_chrome_extension
38
+ attr_reader :disable_fill_missing, :ssl_request
20
39
  alias :disable_fill_missing? :disable_fill_missing
21
40
  alias :ssl_request? :ssl_request
22
41
 
42
+ class << self
43
+ def generate_nonce
44
+ SecureRandom.base64(32).chomp
45
+ end
46
+
47
+ def set_nonce(controller, nonce = generate_nonce)
48
+ controller.instance_variable_set(:@content_security_policy_nonce, nonce)
49
+ end
50
+
51
+ def add_to_env(request, controller, config)
52
+ set_nonce(controller)
53
+ options = options_from_request(request).merge(:controller => controller)
54
+ request.env[Constants::ENV_KEY] = {
55
+ :config => config,
56
+ :options => options,
57
+ }
58
+ end
59
+
60
+ def options_from_request(request)
61
+ {
62
+ :ssl => request.ssl?,
63
+ :ua => request.env['HTTP_USER_AGENT'],
64
+ :request_uri => request_uri_from_request(request),
65
+ }
66
+ end
67
+
68
+ def request_uri_from_request(request)
69
+ if request.respond_to?(:original_url)
70
+ # rails 3.1+
71
+ request.original_url
72
+ else
73
+ # rails 2/3.0
74
+ request.url
75
+ end
76
+ end
77
+
78
+ def symbol_to_hyphen_case sym
79
+ sym.to_s.gsub('_', '-')
80
+ end
81
+ end
82
+
23
83
  # +options+ param contains
24
- # :experimental use experimental block for config
84
+ # :controller used for setting instance variables for nonces/hashes
25
85
  # :ssl_request used to determine if http_additions should be used
26
- # :request_uri used to determine if firefox should send the report directly
27
- # or use the forwarding endpoint
28
86
  # :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
29
87
  #
30
88
  # :report used to determine what :ssl_request, :ua, and :request_uri are set to
31
89
  def initialize(config=nil, options={})
32
- @experimental = !!options.delete(:experimental)
33
- if @experimental
34
- warn "[DEPRECATION] 'experimental' config is removed in 2.0"
35
- end
36
- @controller = options.delete(:controller)
90
+ return unless config
37
91
 
38
92
  if options[:request]
39
- parse_request(options[:request])
40
- else
41
- @ua = options[:ua]
42
- # fails open, assumes http. Bad idea? Will always include http additions.
43
- # could also fail if not supplied.
44
- @ssl_request = !!options.delete(:ssl)
45
- # a nil value here means we always assume we are not on the same host,
46
- # which causes all FF csp reports to go through the forwarder
47
- @request_uri = options.delete(:request_uri)
93
+ options = options.merge(self.class.options_from_request(options[:request]))
48
94
  end
49
95
 
50
- configure(config) if config
51
- end
96
+ @controller = options[:controller]
97
+ @ua = options[:ua]
98
+ @ssl_request = !!options.delete(:ssl)
99
+ @request_uri = options.delete(:request_uri)
52
100
 
53
- def nonce
54
- @nonce ||= SecureRandom.base64(32).chomp
55
- end
101
+ # Config values can be string, array, or lamdba values
102
+ @config = config.inject({}) do |hash, (key, value)|
103
+ config_val = value.respond_to?(:call) ? value.call : value
56
104
 
57
- def configure(config)
58
- @config = config.dup
105
+ if ALL_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
106
+ config_val = config_val.split if config_val.is_a? String
107
+ if config_val.is_a?(Array)
108
+ config_val = config_val.map do |val|
109
+ translate_dir_value(val)
110
+ end.flatten.uniq
111
+ end
112
+ end
59
113
 
60
- experimental_config = @config.delete(:experimental)
61
- if @experimental && experimental_config
62
- @config[:http_additions] = experimental_config[:http_additions]
63
- @config.merge!(experimental_config)
114
+ hash[key] = config_val
115
+ hash
64
116
  end
65
117
 
66
- # these values don't support lambdas because this needs to be rewritten
67
118
  @http_additions = @config.delete(:http_additions)
68
119
  @app_name = @config.delete(:app_name)
120
+ @report_uri = @config.delete(:report_uri)
69
121
 
70
- normalize_csp_options
71
-
72
- META.each do |meta|
73
- self.send("#{meta}=", @config.delete(meta))
74
- end
75
-
122
+ @disable_fill_missing = !!@config.delete(:disable_fill_missing)
76
123
  @enforce = !!@config.delete(:enforce)
77
- @tag_report_uri = @config.delete(:tag_report_uri)
124
+ @tag_report_uri = !!@config.delete(:tag_report_uri)
125
+ @script_hashes = @config.delete(:script_hashes) || []
78
126
 
79
- normalize_reporting_endpoint
127
+ add_script_hashes if @script_hashes.any?
80
128
  fill_directives unless disable_fill_missing?
81
129
  end
82
130
 
131
+ ##
132
+ # Return or initialize the nonce value used for this header.
133
+ # If a reference to a controller is passed in the config, this method
134
+ # will check if a nonce has already been set and use it.
135
+ def nonce
136
+ @nonce ||= @controller.instance_variable_get(:@content_security_policy_nonce) || self.class.generate_nonce
137
+ end
138
+
139
+ ##
140
+ # Returns the name to use for the header. Either "Content-Security-Policy" or
141
+ # "Content-Security-Policy-Report-Only"
83
142
  def name
84
- base = STANDARD_HEADER_NAME
85
- if !@enforce || experimental
143
+ base = HEADER_NAME
144
+ if !@enforce
86
145
  base += "-Report-Only"
87
146
  end
88
147
  base
89
148
  end
90
149
 
150
+ ##
151
+ # Return the value of the CSP header
91
152
  def value
92
153
  return @config if @config.is_a?(String)
93
154
  if @config
@@ -99,26 +160,28 @@ module SecureHeaders
99
160
 
100
161
  private
101
162
 
163
+ def add_script_hashes
164
+ @config[:script_src] << @script_hashes.map {|hash| "'#{hash}'"} << ["'unsafe-inline'"]
165
+ end
166
+
102
167
  def build_value
103
168
  raise "Expected to find default_src directive value" unless @config[:default_src]
104
169
  append_http_additions unless ssl_request?
105
170
  header_value = [
106
- generic_directives(@config),
171
+ generic_directives,
172
+ non_default_directives,
107
173
  report_uri_directive
108
174
  ].join.strip
109
- rescue StandardError => e
110
- raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
111
175
  end
112
176
 
113
177
  def fill_directives
114
- return unless @config[:default_src]
115
- default = @config[:default_src]
116
- DIRECTIVES.each do |directive|
117
- unless @config[directive]
118
- @config[directive] = default
178
+ if default = @config[:default_src]
179
+ DIRECTIVES.each do |directive|
180
+ unless @config[directive]
181
+ @config[directive] = default
182
+ end
119
183
  end
120
184
  end
121
- @config
122
185
  end
123
186
 
124
187
  def append_http_additions
@@ -129,75 +192,20 @@ module SecureHeaders
129
192
  end
130
193
  end
131
194
 
132
- def normalize_csp_options
133
- @config = @config.inject({}) do |hash, (key, value)|
134
- # lambdas
135
- config_val = value.respond_to?(:call) ? value.call : value
136
- # space-delimeted strings
137
- config_val = config_val.split if config_val.is_a? String
138
- # array of strings
139
- if config_val.respond_to?(:map) #skip booleans
140
- config_val = config_val.map do |val|
141
- translate_dir_value(val)
142
- end.flatten.uniq
143
- end
144
-
145
- hash[key] = config_val
146
- hash
147
- end
148
-
149
- @report_uri = @config.delete(:report_uri).join(" ") if @config[:report_uri]
150
- end
151
-
152
- # translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
153
195
  def translate_dir_value val
154
196
  if %w{inline eval}.include?(val)
155
197
  val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'"
156
- # self/none are special sources/src-dir-values and need to be quoted in chrome
198
+ # self/none are special sources/src-dir-values and need to be quoted
157
199
  elsif %{self none}.include?(val)
158
200
  "'#{val}'"
159
201
  elsif val == 'nonce'
160
- @controller.instance_variable_set(:@content_security_policy_nonce, nonce)
202
+ self.class.set_nonce(@controller, nonce)
161
203
  ["'nonce-#{nonce}'", "'unsafe-inline'"]
162
204
  else
163
205
  val
164
206
  end
165
207
  end
166
208
 
167
- # if we have a forwarding endpoint setup and we are not on the same origin as our report_uri
168
- # or only a path was supplied (in which case we assume cross-host)
169
- # we need to forward the request for Firefox.
170
- def normalize_reporting_endpoint
171
- if @ua && @ua =~ /Firefox/
172
- if same_origin? || report_uri.nil? || URI.parse(report_uri).host.nil?
173
- return
174
- end
175
-
176
- if forward_endpoint
177
- warn "[DEPRECATION] forwarder is removed in 2.0"
178
- @report_uri = FF_CSP_ENDPOINT
179
- end
180
- end
181
-
182
- if @tag_report_uri
183
- @report_uri = "#{@report_uri}?enforce=#{@enforce}"
184
- @report_uri += "&app_name=#{@app_name}" if @app_name
185
- end
186
- end
187
-
188
- def same_origin?
189
- return unless report_uri && request_uri
190
-
191
- begin
192
- origin = URI.parse(request_uri)
193
- uri = URI.parse(report_uri)
194
- rescue URI::InvalidURIError
195
- return false
196
- end
197
-
198
- uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
199
- end
200
-
201
209
  def report_uri_directive
202
210
  return '' if @report_uri.nil?
203
211
 
@@ -209,44 +217,40 @@ module SecureHeaders
209
217
  end
210
218
  end
211
219
 
220
+ if @tag_report_uri
221
+ @report_uri = "#{@report_uri}?enforce=#{@enforce}"
222
+ @report_uri += "&app_name=#{@app_name}" if @app_name
223
+ end
224
+
212
225
  "report-uri #{@report_uri};"
213
226
  end
214
227
 
215
- def generic_directives(config)
228
+ def generic_directives
216
229
  header_value = ''
217
- if config[:img_src]
218
- config[:img_src] = config[:img_src] + ['data:'] unless config[:img_src].include?('data:')
230
+ if @config[:img_src]
231
+ @config[:img_src] = @config[:img_src] + ['data:'] unless @config[:img_src].include?('data:')
219
232
  else
220
- config[:img_src] = config[:default_src] + ['data:']
233
+ @config[:img_src] = @config[:default_src] + ['data:']
221
234
  end
222
235
 
223
- header_value = build_directive(:default_src)
224
- config.keys.sort_by{|k| k.to_s}.each do |k| # ensure consistent ordering
225
- header_value += build_directive(k)
236
+ DIRECTIVES.each do |directive_name|
237
+ header_value += build_directive(directive_name) if @config[directive_name]
226
238
  end
227
239
 
228
240
  header_value
229
241
  end
230
242
 
231
- # build and deletes the directive
232
- def build_directive(key)
233
- "#{symbol_to_hyphen_case(key)} #{@config.delete(key).join(" ")}; "
234
- end
243
+ def non_default_directives
244
+ header_value = ''
245
+ NON_DEFAULT_SOURCES.each do |directive_name|
246
+ header_value += build_directive(directive_name) if @config[directive_name]
247
+ end
235
248
 
236
- def symbol_to_hyphen_case sym
237
- sym.to_s.gsub('_', '-')
249
+ header_value
238
250
  end
239
251
 
240
- def parse_request request
241
- @ssl_request = request.ssl?
242
- @ua = request.env['HTTP_USER_AGENT']
243
- @request_uri = if request.respond_to?(:original_url)
244
- # rails 3.1+
245
- request.original_url
246
- else
247
- # rails 2/3.0
248
- request.url
249
- end
252
+ def build_directive(key)
253
+ "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
250
254
  end
251
255
  end
252
256
  end
@@ -16,26 +16,4 @@ else
16
16
  include ::SecureHeaders
17
17
  end
18
18
  end
19
-
20
- module SecureHeaders
21
- module Routing
22
- module MapperExtensions
23
- def csp_endpoint
24
- @set.add_route(ContentSecurityPolicy::FF_CSP_ENDPOINT, {:controller => "content_security_policy", :action => "scribe"})
25
- end
26
- end
27
- end
28
- end
29
-
30
- if defined?(ActiveSupport::Dependencies)
31
- if ActiveSupport::Dependencies.autoload_paths
32
- ActiveSupport::Dependencies.autoload_paths << File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)
33
- else
34
- ActiveSupport::Dependencies.autoload_paths = [File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)]
35
- end
36
- end
37
-
38
- if defined? ActionController::Routing
39
- ActionController::Routing::RouteSet::Mapper.send :include, ::SecureHeaders::Routing::MapperExtensions
40
- end
41
19
  end
@@ -1,3 +1,3 @@
1
1
  module SecureHeaders
2
- VERSION = "1.4.1"
2
+ VERSION = "2.0.0.pre"
3
3
  end
@@ -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,17 @@
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, :x_download_options
8
+ :x_xss_protection, :csp, :x_download_options, :script_hashes
6
9
 
7
10
  def configure &block
8
11
  instance_eval &block
12
+ if File.exists?(SCRIPT_HASH_CONFIG_FILE)
13
+ ::SecureHeaders::Configuration.script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE))
14
+ end
9
15
  end
10
16
  end
11
17
  end
@@ -33,6 +39,7 @@ module SecureHeaders
33
39
 
34
40
  def ensure_security_headers options = {}
35
41
  self.secure_headers_options = options
42
+ before_filter :prep_script_hash
36
43
  before_filter :set_hsts_header
37
44
  before_filter :set_x_frame_options_header
38
45
  before_filter :set_csp_header
@@ -49,7 +56,6 @@ module SecureHeaders
49
56
  end
50
57
 
51
58
  module InstanceMethods
52
- # Re-added for backwards compat.
53
59
  def set_security_headers(options = self.class.secure_headers_options)
54
60
  set_csp_header(request, options[:csp])
55
61
  set_hsts_header(options[:hsts])
@@ -59,28 +65,54 @@ module SecureHeaders
59
65
  set_x_download_options_header(options[:x_download_options])
60
66
  end
61
67
 
62
- # backwards compatibility jank, to be removed in 1.0. Old API required a request
63
- # object when it didn't really need to.
64
68
  # set_csp_header - uses the request accessor and SecureHeader::Configuration settings
65
69
  # set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings
66
70
  # set_csp_header(+Hash+) - uses the request accessor and options from parameters
67
71
  # set_csp_header(+Rack::Request+, +Hash+)
68
- def set_csp_header(req = nil, options=nil)
69
- # hack to help generating headers statically
70
- if req.is_a?(Hash)
71
- options = req
72
+ def set_csp_header(req = nil, config=nil)
73
+ if req.is_a?(Hash) || req.is_a?(FalseClass)
74
+ config = req
72
75
  end
73
76
 
74
- options = self.class.secure_headers_options[:csp] if options.nil?
75
- options = self.class.options_for :csp, options
77
+ config = self.class.secure_headers_options[:csp] if config.nil?
78
+ config = self.class.options_for :csp, config
76
79
 
77
- return if options == false
80
+ return if config == false
81
+
82
+ if config && config[:script_hash_middleware]
83
+ ContentSecurityPolicy.add_to_env(request, self, config)
84
+ else
85
+ csp_header = ContentSecurityPolicy.new(config, :request => request, :controller => self)
86
+ set_header(csp_header)
87
+ end
88
+ end
89
+
90
+
91
+ def prep_script_hash
92
+ if ::SecureHeaders::Configuration.script_hashes
93
+ @script_hashes = ::SecureHeaders::Configuration.script_hashes.dup
94
+ ActiveSupport::Notifications.subscribe("render_partial.action_view") do |event_name, start_at, end_at, id, payload|
95
+ save_hash_for_later payload
96
+ end
78
97
 
79
- csp_header = ContentSecurityPolicy.new(options, :request => request, :controller => self)
80
- set_header(csp_header)
81
- if options && options[:experimental] && options[:enforce]
82
- experimental_header = ContentSecurityPolicy.new(options, :experimental => true, :request => request, :controller => self)
83
- set_header(experimental_header)
98
+ ActiveSupport::Notifications.subscribe("render_template.action_view") do |event_name, start_at, end_at, id, payload|
99
+ save_hash_for_later payload
100
+ end
101
+ end
102
+ end
103
+
104
+ def save_hash_for_later payload
105
+ matching_hashes = @script_hashes[payload[:identifier].gsub(Rails.root.to_s + "/", "")] || []
106
+
107
+ if payload[:layout]
108
+ # 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
109
+ layout_hashes = @script_hashes[File.join("app", "views", payload[:layout]) + '.html.erb']
110
+
111
+ matching_hashes << layout_hashes if layout_hashes
112
+ end
113
+
114
+ if matching_hashes.any?
115
+ request.env[HASHES_ENV_KEY] = ((request.env[HASHES_ENV_KEY] || []) << matching_hashes).flatten
84
116
  end
85
117
  end
86
118
 
@@ -126,7 +158,7 @@ module SecureHeaders
126
158
  end
127
159
  end
128
160
 
129
- require "securerandom"
161
+
130
162
  require "secure_headers/version"
131
163
  require "secure_headers/header"
132
164
  require "secure_headers/headers/content_security_policy"
@@ -136,3 +168,5 @@ require "secure_headers/headers/x_xss_protection"
136
168
  require "secure_headers/headers/x_content_type_options"
137
169
  require "secure_headers/headers/x_download_options"
138
170
  require "secure_headers/railtie"
171
+ require "secure_headers/hash_helper"
172
+ 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