Skip to content

Commit

Permalink
Merge pull request #497 from BetterErrors/feature/content-security-po…
Browse files Browse the repository at this point in the history
…licy

Add Content Security Policy
  • Loading branch information
RobinDaugherty committed Dec 11, 2020
2 parents 133f795 + b9d9ab7 commit 4f58080
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 45 deletions.
4 changes: 4 additions & 0 deletions lib/better_errors/editor.rb
Expand Up @@ -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
Expand Down
38 changes: 25 additions & 13 deletions lib/better_errors/error_page.rb
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -113,19 +125,19 @@ 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

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
"<span class='unsupported'>(Instance of ignored class. "\
"#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
Expand Down
22 changes: 19 additions & 3 deletions lib/better_errors/middleware.rb
Expand Up @@ -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 ]
Expand All @@ -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)
Expand Down
87 changes: 74 additions & 13 deletions lib/better_errors/templates/main.erb
Expand Up @@ -3,7 +3,7 @@
<head>
<title><%= exception_type %> at <%= request_path %></title>
</head>
<body>
<body class="better-errors-javascript-not-loaded">
<%# Stylesheets are placed in the <body> for Turbolinks compatibility. %>
<style>
/* Basic reset */
Expand Down Expand Up @@ -107,13 +107,18 @@
}

.frame_info {
display: none;

right: 0;
left: 40%;

padding: 20px;
padding-left: 10px;
margin-left: 30px;
}
.frame_info.current {
display: block;
}
}

nav.sidebar {
Expand Down Expand Up @@ -227,6 +232,10 @@
* Navigation
* --------------------------------------------------------------------- */

.better-errors-javascript-not-loaded .backtrace .tabs {
display: none;
}

nav.tabs {
border-bottom: solid 1px #ddd;

Expand Down Expand Up @@ -411,6 +420,18 @@
* Display area
* --------------------------------------------------------------------- */

p.no-javascript-notice {
margin-bottom: 1em;
padding: 1em;
border: 2px solid #e00;
}
.better-errors-javascript-loaded .no-javascript-notice {
display: none;
}
.no-inline-style-notice {
display: none;
}

.trace_info {
background: #fff;
padding: 6px;
Expand Down Expand Up @@ -468,6 +489,10 @@
font-weight: 200;
}

.better-errors-javascript-not-loaded .be-repl {
display: none;
}

.code, .be-console, .unavailable {
background: #fff;
padding: 5px;
Expand Down Expand Up @@ -598,6 +623,9 @@
.console-has-been-used .live-console-hint {
display: none;
}
.better-errors-javascript-not-loaded .live-console-hint {
display: none;
}

.hint:before {
content: '\25b2';
Expand Down Expand Up @@ -701,7 +729,7 @@
</style>

<%# IE8 compatibility crap %>
<script>
<script nonce="<%= csp_nonce %>">
(function() {
var elements = ["section", "nav", "header", "footer", "audio"];
for (var i = 0; i < elements.length; i++) {
Expand All @@ -715,7 +743,7 @@
rendered in the host app's layout. Let's empty out the styles of the
host app.
%>
<script>
<script nonce="<%= csp_nonce %>">
if (window.Turbolinks) {
for(var i=0; i < document.styleSheets.length; i++) {
if(document.styleSheets[i].href)
Expand All @@ -740,6 +768,15 @@
}
</script>

<p class='no-inline-style-notice'>
<strong>
Better Errors can't apply inline style<span class='no-javascript-notice'> (or run Javascript)</span>,
possibly because you have a Content Security Policy along with Turbolinks.
But you can
<a href='/__better_errors' target="_blank">open the interactive console in a new tab/window</a>.
</strong>
</p>

<div class='top'>
<header class="exception">
<h2><strong><%= exception_type %></strong> <span>at <%= request_path %></span></h2>
Expand Down Expand Up @@ -786,21 +823,37 @@
</ul>
</nav>

<% backtrace_frames.each_with_index do |frame, index| %>
<div class="frame_info" id="frame_info_<%= index %>" style="display:none;"></div>
<% end %>
<div class="frameInfos">
<div class="frame_info current" data-frame-idx="0">
<p class='no-javascript-notice'>
Better Errors can't run Javascript here<span class='no-inline-style-notice'> (or apply inline style)</span>,
possibly because you have a Content Security Policy along with Turbolinks.
But you can
<a href='/__better_errors' target="_blank">open the interactive console in a new tab/window</a>.
</p>
<!-- this is enough information to show something in case JS doesn't get to load -->
<%== ErrorPage.render_template('variable_info', first_frame_variable_info) %>
</div>
</div>
</section>
</body>
<script>
<script nonce="<%= csp_nonce %>">
(function() {

var OID = "<%= id %>";
var csrfToken = "<%= csrf_token %>";

var previousFrame = null;
var previousFrameInfo = null;
var allFrames = document.querySelectorAll("ul.frames li");
var allFrameInfos = document.querySelectorAll(".frame_info");
var frameInfos = document.querySelector(".frameInfos");

document.querySelector('body').classList.remove("better-errors-javascript-not-loaded");
document.querySelector('body').classList.add("better-errors-javascript-loaded");

var noJSNotices = document.querySelectorAll('.no-javascript-notice');
for(var i = 0; i < noJSNotices.length; i++) {
noJSNotices[i].remove();
}

function apiCall(method, opts, cb) {
var req = new XMLHttpRequest();
Expand Down Expand Up @@ -974,17 +1027,25 @@
};

function switchTo(el) {
if(previousFrameInfo) previousFrameInfo.style.display = "none";
previousFrameInfo = el;
var currentFrameInfo = document.querySelectorAll('.frame_info.current');
for(var i = 0; i < currentFrameInfo.length; i++) {
currentFrameInfo[i].className = "frame_info";
}

el.style.display = "block";
el.className = "frame_info current";

var replInput = el.querySelector('.be-console input');
if (replInput) replInput.focus();
}

function selectFrameInfo(index) {
var el = allFrameInfos[index];
var el = document.querySelector(".frame_info[data-frame-idx='" + index + "']")
if (!el) {
el = document.createElement("div");
el.className = "frame_info";
el.setAttribute('data-frame-idx', index);
frameInfos.appendChild(el);
}
if(el) {
if (el.loaded) {
return switchTo(el);
Expand Down
2 changes: 1 addition & 1 deletion lib/better_errors/templates/text.erb
Expand Up @@ -9,7 +9,7 @@
<%== text_heading("-", "%s, line %i" % [first_frame.pretty_path, first_frame.line]) %>

``` ruby
<%== text_formatted_code_block(first_frame) %>```
<%== ErrorPage.text_formatted_code_block(first_frame) %>```

App backtrace
-------------
Expand Down

0 comments on commit 4f58080

Please sign in to comment.