Skip to content

Commit aa073f6

Browse files
Validate internal call method names
1 parent 446c192 commit aa073f6

File tree

2 files changed

+158
-6
lines changed

2 files changed

+158
-6
lines changed

lib/better_errors/middleware.rb

Lines changed: 12 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]
@@ -158,7 +159,7 @@ def internal_call(env, opts)
158159

159160
return not_acceptable_json_response unless request.content_type == 'application/json'
160161

161-
response = @error_page.send("do_#{opts[:method]}", body)
162+
response = @error_page.send("do_#{method}", body)
162163
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(response)]]
163164
end
164165

@@ -203,6 +204,13 @@ def invalid_csrf_token_json_response
203204
)]]
204205
end
205206

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+
206214
def not_acceptable_json_response
207215
[406, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
208216
error: "Request not acceptable",

spec/better_errors/middleware_spec.rb

Lines changed: 146 additions & 2 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

@@ -384,7 +384,131 @@ def initialize(message, original_exception = nil)
384384
end
385385

386386
context 'when the body csrfToken does not match the CSRF token cookie' do
387-
let(:request_body_data) { {"index": 0, "csrfToken": "csrfToken123"} }
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
402+
expect(json_body).to match(
403+
'error' => 'Invalid CSRF Token',
404+
'explanation' => /session might have been cleared/,
405+
)
406+
end
407+
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
509+
510+
context 'when the body csrfToken does not match the CSRF token cookie' do
511+
let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
388512
before do
389513
request_env["HTTP_COOKIE"] = "BetterErrors-CSRF-Token=csrfToken456"
390514
end
@@ -408,5 +532,25 @@ def initialize(message, original_exception = nil)
408532
end
409533
end
410534
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
411555
end
412556
end

0 commit comments

Comments
 (0)