Skip to content

Commit 50b4257

Browse files
Merge pull request #475 from BetterErrors/feature/correct-xhr-mime-type
Validate internal request method names
2 parents 8e8e796 + aa073f6 commit 50b4257

File tree

2 files changed

+189
-9
lines changed

2 files changed

+189
-9
lines changed

lib/better_errors/middleware.rb

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def allow_ip?(env)
7575
def better_errors_call(env)
7676
case env["PATH_INFO"]
7777
when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z}
78-
internal_call env, $~
78+
internal_call(env, $~[:id], $~[:method])
7979
when %r{/__better_errors/?\z}
8080
show_error_page env
8181
else
@@ -145,9 +145,10 @@ def backtrace_frames
145145
end
146146
end
147147

148-
def internal_call(env, opts)
148+
def internal_call(env, id, method)
149+
return not_found_json_response unless %w[variables eval].include?(method)
149150
return no_errors_json_response unless @error_page
150-
return invalid_error_json_response if opts[:id] != @error_page.id
151+
return invalid_error_json_response if id != @error_page.id
151152

152153
request = Rack::Request.new(env)
153154
return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
@@ -156,7 +157,9 @@ def internal_call(env, opts)
156157
body = JSON.parse(request.body.read)
157158
return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME] == body['csrfToken']
158159

159-
response = @error_page.send("do_#{opts[:method]}", body)
160+
return not_acceptable_json_response unless request.content_type == 'application/json'
161+
162+
response = @error_page.send("do_#{method}", body)
160163
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(response)]]
161164
end
162165

@@ -200,5 +203,19 @@ def invalid_csrf_token_json_response
200203
"or something went wrong.",
201204
)]]
202205
end
206+
207+
def not_found_json_response
208+
[404, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
209+
error: "Not found",
210+
explanation: "Not a recognized internal call.",
211+
)]]
212+
end
213+
214+
def not_acceptable_json_response
215+
[406, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
216+
error: "Request not acceptable",
217+
explanation: "The internal request did not match an acceptable content type.",
218+
)]]
219+
end
203220
end
204221
end

spec/better_errors/middleware_spec.rb

Lines changed: 168 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def initialize(message, original_exception = nil)
294294
let(:request_env) {
295295
Rack::MockRequest.env_for("/__better_errors/#{id}/variables", input: StringIO.new(JSON.dump(request_body_data)))
296296
}
297-
let(:request_body_data) { {"index": 0} }
297+
let(:request_body_data) { { "index" => 0 } }
298298
let(:json_body) { JSON.parse(body) }
299299
let(:id) { 'abcdefg' }
300300

@@ -356,16 +356,159 @@ def initialize(message, original_exception = nil)
356356
request_env["HTTP_COOKIE"] = "BetterErrors-CSRF-Token=csrfToken123"
357357
end
358358

359-
it 'returns the HTML content' do
360-
expect(error_page).to receive(:do_variables).and_return(html: "<content>")
359+
context 'when the Content-Type of the request is application/json' do
360+
before do
361+
request_env['CONTENT_TYPE'] = 'application/json'
362+
end
363+
364+
it 'returns JSON containing the HTML content' do
365+
expect(error_page).to receive(:do_variables).and_return(html: "<content>")
366+
expect(json_body).to match(
367+
'html' => '<content>',
368+
)
369+
end
370+
end
371+
372+
context 'when the Content-Type of the request is application/json' do
373+
before do
374+
request_env['HTTP_CONTENT_TYPE'] = 'application/json'
375+
end
376+
377+
it 'returns a JSON error' do
378+
expect(json_body).to match(
379+
'error' => 'Request not acceptable',
380+
'explanation' => /did not match an acceptable content type/,
381+
)
382+
end
383+
end
384+
end
385+
386+
context 'when the body csrfToken does not match the CSRF token cookie' do
387+
let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
388+
before do
389+
request_env["HTTP_COOKIE"] = "BetterErrors-CSRF-Token=csrfToken456"
390+
end
391+
392+
it 'returns a JSON error' do
393+
expect(json_body).to match(
394+
'error' => 'Invalid CSRF Token',
395+
'explanation' => /session might have been cleared/,
396+
)
397+
end
398+
end
399+
400+
context 'when there is no CSRF token in the request' do
401+
it 'returns a JSON error' do
361402
expect(json_body).to match(
362-
'html' => '<content>',
403+
'error' => 'Invalid CSRF Token',
404+
'explanation' => /session might have been cleared/,
363405
)
364406
end
365407
end
408+
end
409+
end
410+
end
411+
412+
context "requesting eval for a specific frame" do
413+
let(:env) { {} }
414+
let(:response_env) {
415+
app.call(request_env)
416+
}
417+
let(:request_env) {
418+
Rack::MockRequest.env_for("/__better_errors/#{id}/eval", input: StringIO.new(JSON.dump(request_body_data)))
419+
}
420+
let(:request_body_data) { { "index" => 0, source: "do_a_thing" } }
421+
let(:json_body) { JSON.parse(body) }
422+
let(:id) { 'abcdefg' }
423+
424+
context 'when no errors have been recorded' do
425+
it 'returns a JSON error' do
426+
expect(json_body).to match(
427+
'error' => 'No exception information available',
428+
'explanation' => /application has been restarted/,
429+
)
430+
end
431+
432+
context 'when Middleman is in use' do
433+
let!(:middleman) { class_double("Middleman").as_stubbed_const }
434+
it 'returns a JSON error' do
435+
expect(json_body['explanation'])
436+
.to match(/Middleman reloads all dependencies/)
437+
end
438+
end
439+
440+
context 'when Shotgun is in use' do
441+
let!(:shotgun) { class_double("Shotgun").as_stubbed_const }
442+
443+
it 'returns a JSON error' do
444+
expect(json_body['explanation'])
445+
.to match(/The shotgun gem/)
446+
end
447+
448+
context 'when Hanami is also in use' do
449+
let!(:hanami) { class_double("Hanami").as_stubbed_const }
450+
it 'returns a JSON error' do
451+
expect(json_body['explanation'])
452+
.to match(/--no-code-reloading/)
453+
end
454+
end
455+
end
456+
end
457+
458+
context 'when an error has been recorded' do
459+
let(:error_page) { ErrorPage.new(exception, env) }
460+
before do
461+
app.instance_variable_set('@error_page', error_page)
462+
end
463+
464+
context 'but it does not match the request' do
465+
it 'returns a JSON error' do
466+
expect(json_body).to match(
467+
'error' => 'Session expired',
468+
'explanation' => /no longer available in memory/,
469+
)
470+
end
471+
end
472+
473+
context 'and its ID matches the requested ID' do
474+
let(:id) { error_page.id }
475+
476+
context 'when the body csrfToken matches the CSRF token cookie' do
477+
let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
478+
before do
479+
request_env["HTTP_COOKIE"] = "BetterErrors-CSRF-Token=csrfToken123"
480+
end
481+
482+
context 'when the Content-Type of the request is application/json' do
483+
before do
484+
request_env['CONTENT_TYPE'] = 'application/json'
485+
end
486+
487+
it 'returns JSON containing the eval result' do
488+
expect(error_page).to receive(:do_eval).and_return(prompt: '#', result: "much_stuff_here")
489+
expect(json_body).to match(
490+
'prompt' => '#',
491+
'result' => 'much_stuff_here',
492+
)
493+
end
494+
end
495+
496+
context 'when the Content-Type of the request is application/json' do
497+
before do
498+
request_env['HTTP_CONTENT_TYPE'] = 'application/json'
499+
end
500+
501+
it 'returns a JSON error' do
502+
expect(json_body).to match(
503+
'error' => 'Request not acceptable',
504+
'explanation' => /did not match an acceptable content type/,
505+
)
506+
end
507+
end
508+
end
366509

367510
context 'when the body csrfToken does not match the CSRF token cookie' do
368-
let(:request_body_data) { {"index": 0, "csrfToken": "csrfToken123"} }
511+
let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
369512
before do
370513
request_env["HTTP_COOKIE"] = "BetterErrors-CSRF-Token=csrfToken456"
371514
end
@@ -389,5 +532,25 @@ def initialize(message, original_exception = nil)
389532
end
390533
end
391534
end
535+
536+
context "requesting an invalid internal method" do
537+
let(:env) { {} }
538+
let(:response_env) {
539+
app.call(request_env)
540+
}
541+
let(:request_env) {
542+
Rack::MockRequest.env_for("/__better_errors/#{id}/invalid", input: StringIO.new(JSON.dump(request_body_data)))
543+
}
544+
let(:request_body_data) { { "index" => 0 } }
545+
let(:json_body) { JSON.parse(body) }
546+
let(:id) { 'abcdefg' }
547+
548+
it 'returns a JSON error' do
549+
expect(json_body).to match(
550+
'error' => 'Not found',
551+
'explanation' => /recognized internal call/,
552+
)
553+
end
554+
end
392555
end
393556
end

0 commit comments

Comments
 (0)