Don't show error messages that could contain private information.
Fix a potential exploit where private information could be leaked if
it was contained in the error message of an unexpected exception.
For example, NoMethodError contains a raw dump of the object in the
error message, which could leak private user data if you could force a
User object to raise a NoMethodError.
Fix the error page to only show known-safe error messages from expected
exceptions, not unknown error messages from unexpected exceptions.
API changes:
* JSON errors now have a `message` param. The message will be blank for unknown exceptions.
* XML errors have a new format. This is a breaking change. They now look like this:
<result>
<success type="boolean">false</success>
<error>PaginationExtension::PaginationError</error>
<message>You cannot go beyond page 5000.</message>
<backtrace type="array">
<backtrace>app/logical/pagination_extension.rb:54:in `paginate'</backtrace>
<backtrace>app/models/application_record.rb:17:in `paginate'</backtrace>
<backtrace>app/logical/post_query_builder.rb:529:in `paginated_posts'</backtrace>
<backtrace>app/logical/post_sets/post.rb:95:in `posts'</backtrace>
<backtrace>app/controllers/posts_controller.rb:22:in `index'</backtrace>
</backtrace>
</result>
instead of like this:
<result success="false">You cannot go beyond page 5000.</result>
This commit is contained in:
@@ -112,11 +112,11 @@ class ApplicationController < ActionController::Base
|
||||
when ActiveRecord::QueryCanceled
|
||||
render_error_page(500, exception, template: "static/search_timeout", message: "The database timed out running your query.")
|
||||
when ActionController::BadRequest
|
||||
render_error_page(400, exception)
|
||||
render_error_page(400, exception, message: exception.message)
|
||||
when SessionLoader::AuthenticationFailure
|
||||
render_error_page(401, exception, template: "sessions/new")
|
||||
render_error_page(401, exception, message: exception.message, template: "sessions/new")
|
||||
when ActionController::InvalidAuthenticityToken, ActionController::UnpermittedParameters, ActionController::InvalidCrossOriginRequest, ActionController::Redirecting::UnsafeRedirectError
|
||||
render_error_page(403, exception)
|
||||
render_error_page(403, exception, message: exception.message)
|
||||
when ActiveSupport::MessageVerifier::InvalidSignature, # raised by `find_signed!`
|
||||
User::PrivilegeError,
|
||||
Pundit::NotAuthorizedError
|
||||
@@ -124,7 +124,7 @@ class ApplicationController < ActionController::Base
|
||||
when ActiveRecord::RecordNotFound
|
||||
render_error_page(404, exception, message: "That record was not found.")
|
||||
when ActionController::RoutingError
|
||||
render_error_page(405, exception)
|
||||
render_error_page(405, exception, message: exception.message)
|
||||
when ActionController::UnknownFormat, ActionView::MissingTemplate
|
||||
render_error_page(406, exception, message: "#{request.format} is not a supported format for this page")
|
||||
when PaginationExtension::PaginationError
|
||||
@@ -132,7 +132,7 @@ class ApplicationController < ActionController::Base
|
||||
when PostQueryBuilder::TagLimitError
|
||||
render_error_page(422, exception, template: "static/tag_limit_error", message: "You cannot search for more than #{CurrentUser.tag_query_limit} tags at a time.")
|
||||
when RateLimiter::RateLimitError
|
||||
render_error_page(429, exception)
|
||||
render_error_page(429, exception, message: "Rate limit exceeded. You're doing that too fast")
|
||||
when Rack::Timeout::RequestTimeoutException
|
||||
render_error_page(500, exception, message: "Your request took too long to complete and was canceled.")
|
||||
when NotImplementedError
|
||||
@@ -140,18 +140,20 @@ class ApplicationController < ActionController::Base
|
||||
when PG::ConnectionBad
|
||||
render_error_page(503, exception, message: "The database is unavailable. Try again later.")
|
||||
else
|
||||
raise exception if !Rails.env.production? || Danbooru.config.debug_mode
|
||||
raise exception if Rails.env.development? || Danbooru.config.debug_mode
|
||||
render_error_page(500, exception)
|
||||
end
|
||||
end
|
||||
|
||||
def render_error_page(status, exception = nil, message: exception.message, template: "static/error", format: request.format.symbol)
|
||||
def render_error_page(status, exception = nil, message: "", template: "static/error", format: request.format.symbol)
|
||||
@exception = exception
|
||||
@expected = status < 500
|
||||
@message = message.encode("utf-8", invalid: :replace, undef: :replace)
|
||||
@message = message.to_s.encode("utf-8", invalid: :replace, undef: :replace)
|
||||
@backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace) if @exception
|
||||
format = :html unless format.in?(%i[html json xml js atom])
|
||||
|
||||
@api_response = { success: false, error: @exception.class.to_s, message: @message, backtrace: @backtrace }
|
||||
|
||||
# if InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to use the blank layout.
|
||||
layout = CurrentUser.user.present? ? "default" : "blank"
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class SessionsController < ApplicationController
|
||||
respond_with(user, location: url)
|
||||
else
|
||||
flash.now[:notice] = "Password was incorrect"
|
||||
raise SessionLoader::AuthenticationFailure
|
||||
raise SessionLoader::AuthenticationFailure, "Username or password incorrect"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ class SessionLoader
|
||||
elsif params[:login].present? && params[:api_key].present?
|
||||
authenticate_api_key(params[:login], params[:api_key])
|
||||
else
|
||||
raise AuthenticationFailure
|
||||
raise AuthenticationFailure, "Missing `login` or `api_key`"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -129,7 +129,7 @@ class SessionLoader
|
||||
# permissions for this endpoint
|
||||
def authenticate_api_key(name, key)
|
||||
user, api_key = User.find_by_name(name)&.authenticate_api_key(key)
|
||||
raise AuthenticationFailure if user.blank?
|
||||
raise AuthenticationFailure, "Invalid API key" if user.blank?
|
||||
update_api_key(api_key)
|
||||
raise User::PrivilegeError if !api_key.has_permission?(request.remote_ip, request.params[:controller], request.params[:action])
|
||||
CurrentUser.user = user
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<%# backtrace %>
|
||||
|
||||
<ul class="backtrace font-monospace p-4 mb-4">
|
||||
<% if defined?(exception) %>
|
||||
<li><%= exception.class.to_s %> exception raised</li>
|
||||
<% end %>
|
||||
|
||||
<% Rails.backtrace_cleaner.clean(backtrace).each do |b| %>
|
||||
<li class="backtrace-line"><%= b %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
@@ -0,0 +1 @@
|
||||
<%= raw @api_response.to_xml(root: "result") %>
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
<% page_title "Error: #{@message}" %>
|
||||
<% page_title "Error: #{@exception.class}" %>
|
||||
|
||||
<h1>Error</h1>
|
||||
<p><%= @message %></p>
|
||||
|
||||
<% unless @expected %>
|
||||
<% if @message.present? %>
|
||||
<p><%= @message %></p>
|
||||
<% else %>
|
||||
<p>Unexpected error: <%= @exception.class %>.</p>
|
||||
|
||||
<h3>Details</h3>
|
||||
<%= render "static/backtrace", exception: @exception, backtrace: @backtrace %>
|
||||
|
||||
<ul class="backtrace font-monospace p-4 mb-4">
|
||||
<li><%= @exception.class.to_s %> exception raised</li>
|
||||
|
||||
<% Rails.backtrace_cleaner.clean(@backtrace).each do |b| %>
|
||||
<li class="backtrace-line"><%= b %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
var klass = <%= raw @exception.class.to_s.to_json %>;
|
||||
var message = <%= raw @message.to_json %>;
|
||||
var backtrace = <%= raw @backtrace.to_json %>;
|
||||
console.error(<%= raw @api_response.to_json %>);
|
||||
|
||||
<% if @expected %>
|
||||
Danbooru.Utility.error(message);
|
||||
<% if @message.present? %>
|
||||
Danbooru.Utility.error("<%= j @message %>");
|
||||
<% else %>
|
||||
console.error(klass, message, backtrace);
|
||||
Danbooru.Utility.error(message);
|
||||
Danbooru.Utility.error("<%= j "Unexpected error: #{@exception.class}" %>");
|
||||
<% end %>
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
{
|
||||
"success": false,
|
||||
"message": <%= raw @message.to_json %>,
|
||||
"backtrace": <%= raw @backtrace.to_json %>
|
||||
}
|
||||
<%= raw @api_response.to_json %>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<result success="false"><%= @message %></result>
|
||||
<%= raw @api_response.to_xml(root: "result") %>
|
||||
|
||||
@@ -36,6 +36,45 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
context "on an unexpected error" do
|
||||
setup do
|
||||
User.stubs(:find).raises(NoMethodError.new("pwned"))
|
||||
@user = create(:user)
|
||||
end
|
||||
|
||||
should "not return the error message in the HTML response" do
|
||||
get user_path(@user)
|
||||
|
||||
assert_response 500
|
||||
assert_match(/NoMethodError/, response.body.to_s)
|
||||
assert_no_match(/pwned/, response.body.to_s)
|
||||
end
|
||||
|
||||
should "not return the error message in the JSON response" do
|
||||
get user_path(@user, format: :json)
|
||||
|
||||
assert_response 500
|
||||
assert_match(/NoMethodError/, response.body.to_s)
|
||||
assert_no_match(/pwned/, response.body.to_s)
|
||||
end
|
||||
|
||||
should "not return the error message in the XML response" do
|
||||
get user_path(@user, format: :xml)
|
||||
|
||||
assert_response 500
|
||||
assert_match(/NoMethodError/, response.body.to_s)
|
||||
assert_no_match(/pwned/, response.body.to_s)
|
||||
end
|
||||
|
||||
should "not return the error message in the JS response" do
|
||||
get user_path(@user, format: :js)
|
||||
|
||||
assert_response 500
|
||||
assert_match(/NoMethodError/, response.body.to_s)
|
||||
assert_no_match(/pwned/, response.body.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context "on api authentication" do
|
||||
setup do
|
||||
@user = create(:user, password: "password")
|
||||
|
||||
@@ -68,7 +68,7 @@ class StaticControllerTest < ActionDispatch::IntegrationTest
|
||||
get "/qwoiqogieqg", as: :xml
|
||||
|
||||
assert_response 404
|
||||
assert_equal("Page not found", response.parsed_body.at("result").text)
|
||||
assert_equal("Page not found", response.parsed_body.xpath("result/message").text)
|
||||
end
|
||||
|
||||
should "render the 404 page when page_not_found_pool_id is configured" do
|
||||
|
||||
Reference in New Issue
Block a user