secure_headers 1.4.1 → 2.0.0.pre

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.

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