diff --git a/lib/better_errors/editor.rb b/lib/better_errors/editor.rb index b8de8fb7..96ffc3f0 100644 --- a/lib/better_errors/editor.rb +++ b/lib/better_errors/editor.rb @@ -84,6 +84,10 @@ def url(raw_path, line) url_proc.call(file, line) end + def scheme + url('/fake', 42).sub(/:.*/, ':') + end + private attr_reader :url_proc diff --git a/lib/better_errors/error_page.rb b/lib/better_errors/error_page.rb index 1e44bd6e..91a8c2d2 100644 --- a/lib/better_errors/error_page.rb +++ b/lib/better_errors/error_page.rb @@ -5,6 +5,8 @@ module BetterErrors # @private class ErrorPage + VariableInfo = Struct.new(:frame, :editor_url, :rails_params, :rack_session, :start_time) + def self.template_path(template_name) File.expand_path("../templates/#{template_name}.erb", __FILE__) end @@ -13,6 +15,15 @@ def self.template(template_name) Erubi::Engine.new(File.read(template_path(template_name)), escape: true) end + def self.render_template(template_name, locals) + locals.send(:eval, self.template(template_name).src) + rescue => e + # Fix the backtrace, which doesn't identify the template that failed (within Better Errors). + # We don't know the line number, so just injecting the template path has to be enough. + e.backtrace.unshift "#{self.template_path(template_name)}:0" + raise + end + attr_reader :exception, :env, :repls def initialize(exception, env) @@ -26,20 +37,21 @@ def id @id ||= SecureRandom.hex(8) end - def render(template_name = "main", csrf_token = nil) - binding.eval(self.class.template(template_name).src) - rescue => e - # Fix the backtrace, which doesn't identify the template that failed (within Better Errors). - # We don't know the line number, so just injecting the template path has to be enough. - e.backtrace.unshift "#{self.class.template_path(template_name)}:0" - raise + def render_main(csrf_token, csp_nonce) + frame = backtrace_frames[0] + first_frame_variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f) + self.class.render_template('main', binding) + end + + def render_text + self.class.render_template('text', binding) end def do_variables(opts) index = opts["index"].to_i - @frame = backtrace_frames[index] - @var_start_time = Time.now.to_f - { html: render("variable_info") } + frame = backtrace_frames[index] + variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f) + { html: self.class.render_template("variable_info", variable_info) } end def do_eval(opts) @@ -113,11 +125,11 @@ def request_path env["PATH_INFO"] end - def html_formatted_code_block(frame) + def self.html_formatted_code_block(frame) CodeFormatter::HTML.new(frame.filename, frame.line).output end - def text_formatted_code_block(frame) + def self.text_formatted_code_block(frame) CodeFormatter::Text.new(frame.filename, frame.line).output end @@ -125,7 +137,7 @@ def text_heading(char, str) str + "\n" + char*str.size end - def inspect_value(obj) + def self.inspect_value(obj) if BetterErrors.ignored_classes.include? obj.class.name "(Instance of ignored class. "\ "#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\ diff --git a/lib/better_errors/middleware.rb b/lib/better_errors/middleware.rb index 3a437819..7a2ebbeb 100644 --- a/lib/better_errors/middleware.rb +++ b/lib/better_errors/middleware.rb @@ -94,12 +94,13 @@ def protected_app_call(env) def show_error_page(env, exception=nil) request = Rack::Request.new(env) csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid + csp_nonce = SecureRandom.base64(12) type, content = if @error_page if text?(env) - [ 'plain', @error_page.render('text') ] + [ 'plain', @error_page.render_text ] else - [ 'html', @error_page.render('main', csrf_token) ] + [ 'html', @error_page.render_main(csrf_token, csp_nonce) ] end else [ 'html', no_errors_page ] @@ -110,7 +111,22 @@ def show_error_page(env, exception=nil) status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code end - response = Rack::Response.new(content, status_code, { "Content-Type" => "text/#{type}; charset=utf-8" }) + headers = { + "Content-Type" => "text/#{type}; charset=utf-8", + "Content-Security-Policy" => [ + "default-src 'none'", + # Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set + # for older browsers without nonce support. + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src + "script-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'", + # Inline style is required by the syntax highlighter. + "style-src 'self' 'unsafe-inline'", + "connect-src 'self'", + "navigate-to 'self' #{BetterErrors.editor.scheme}", + ].join('; '), + } + + response = Rack::Response.new(content, status_code, headers) unless request.cookies[CSRF_TOKEN_COOKIE_NAME] response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, path: "/", httponly: true, same_site: :strict) diff --git a/lib/better_errors/templates/main.erb b/lib/better_errors/templates/main.erb index c282c29f..5c1c71dd 100644 --- a/lib/better_errors/templates/main.erb +++ b/lib/better_errors/templates/main.erb @@ -3,7 +3,7 @@ <%= exception_type %> at <%= request_path %> - + <%# Stylesheets are placed in the for Turbolinks compatibility. %> <%# IE8 compatibility crap %> - +

+ + Better Errors can't apply inline style (or run Javascript), + possibly because you have a Content Security Policy along with Turbolinks. + But you can + open the interactive console in a new tab/window. + +

+

<%= exception_type %> at <%= request_path %>

@@ -786,21 +823,37 @@ - <% backtrace_frames.each_with_index do |frame, index| %> - - <% end %> +
+
+

+ Better Errors can't run Javascript here (or apply inline style), + possibly because you have a Content Security Policy along with Turbolinks. + But you can + open the interactive console in a new tab/window. +

+ + <%== ErrorPage.render_template('variable_info', first_frame_variable_info) %> +
+
-