From 66dee5e0f92bec8db907ef09156308168c711dab Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Fri, 11 Dec 2020 10:43:42 -0500 Subject: [PATCH 1/7] Content Security Policy for inline scripts --- lib/better_errors/error_page.rb | 2 +- lib/better_errors/middleware.rb | 21 +++++++++++++++++++-- lib/better_errors/templates/main.erb | 6 +++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/better_errors/error_page.rb b/lib/better_errors/error_page.rb index 1e44bd6e..2ff3cc15 100644 --- a/lib/better_errors/error_page.rb +++ b/lib/better_errors/error_page.rb @@ -26,7 +26,7 @@ def id @id ||= SecureRandom.hex(8) end - def render(template_name = "main", csrf_token = nil) + def render(template_name = "main", csrf_token = nil, csp_nonce = 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). diff --git a/lib/better_errors/middleware.rb b/lib/better_errors/middleware.rb index 3a437819..a95d255b 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') ] 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,23 @@ 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 'self' https:", # TODO: remove https:? + "font-src 'self' https: data:", + "img-src 'self' https: data:", + "object-src 'none'", + # Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set + # for older browsers without nonce support. + "script-src 'self' https: 'nonce-#{csp_nonce}' 'unsafe-inline'", + # Inline style is required by the syntax highlighter. + "style-src 'self' https: 'unsafe-inline'", + "connect-src 'self' https:", + ].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..57bbdc1c 100644 --- a/lib/better_errors/templates/main.erb +++ b/lib/better_errors/templates/main.erb @@ -701,7 +701,7 @@ <%# IE8 compatibility crap %> - +

+ + Better Errors can't 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. + +

+

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

@@ -791,9 +823,18 @@ - <% 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) %> +
+