Information Disclosure via exception backtrace in error handling logs/notifications

HIGH
rails/rails
Commit: b47cc51b989e
Affected: 8.1.0 - 8.1.2 (pre-fix); 8.1.3+ includes the fix in this commit
2026-04-05 12:39 UTC

Description

The commit changes Action Pack's rescue_from handling to stop emitting exception backtrace information in the structured event payload for rescue_from events. Previously, the first line of the exception backtrace (sanitized by removing Rails.root) could be included in the event payload (and thus logged/observed via ActiveSupport notifications), leaking server filesystem paths (Rails.root) and other sensitive path information. The patch removes the backtrace from the emitted event payload entirely, reducing information disclosure risk via error handling logs/notifications.

Proof of Concept

PoC to demonstrate information disclosure via exception backtrace in logs/notifications before the fix: Assumptions: - Rails.root = "/var/www/myapp" - An exception occurs during request handling and is caught by rescue_from, which emits a structured event that includes exception backtrace information. Before fix (conceptual): - The first line of the exception backtrace is included in the event payload (with Rails.root prefix removed): exception_backtrace: "/app/controllers/foo_controller.rb:42:in `index'" # (example path after sanitizing Rails.root) - This path could be logged or sent to a monitoring service and disclose the repository layout on the server. PoC Ruby-IRB-like snippet (illustrative, not a running Rails app): #!/usr/bin/env ruby root = "/var/www/myapp" # Rails.root begin raise "boom" rescue => e # BEFORE FIX: backtrace may be sent in logs/notifications first_line = e.backtrace&.first backtrace_payload = first_line&.delete_prefix("#{root}/") puts "Backtrace payload (potential disclosure): #{backtrace_payload}" end After fix (conceptual): - The payload would no longer include exception_backtrace, or it would be nil/empty, preventing leakage of the filesystem path. PoC exploit vector (attack path): - An attacker does not directly execute code to read files, but relies on error handling/logging/monitoring that captures structured_event payloads containing exception_backtrace. - If access to logs/monitoring is not properly restricted, the attacker could glean server filesystem layout (Rails.root and surrounding paths) from the backtrace line, aiding targeted reconnaissance or further exploits. Mitigation verification steps: - Trigger an error in production with rescue_from that emits the unsafe payload and observe logs/notification payloads before/after this commit. - Ensure that the exception_backtrace field is not present or is null in the emitted events after the fix.

Commit Details

Author: Hartley McGuire

Date: 2025-11-02 18:19 UTC

Message:

Merge pull request #56075 from skipkayhil/hm-ykzwzxuypxrsypvs Various event related fixes

Triage Assessment

Vulnerability Type: Information disclosure

Confidence: HIGH

Reasoning:

The changes sanitize exception backtraces before emitting rescue_from events, removing sensitive path information (Rails.root) from logs/notifications. This reduces information disclosure risk via stack traces in error handling, addressing a security-related exposure. The rest of the changes are minor or test scaffolding, but the backtrace sanitization constitutes the security fix.

Verification Assessment

Vulnerability Type: Information Disclosure via exception backtrace in error handling logs/notifications

Confidence: HIGH

Affected Versions: 8.1.0 - 8.1.2 (pre-fix); 8.1.3+ includes the fix in this commit

Code Diff

diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 7a949033c3f4f..f1a5d3a67bfbe 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -117,7 +117,7 @@ def allowed_redirect_hosts=(hosts) # The `action_on_open_redirect` configuration option controls the behavior when an unsafe # redirect is detected: # * `:log` - Logs a warning but allows the redirect - # * `:notify` - Sends an ActiveSupport notification for monitoring + # * `:notify` - Sends an Active Support notification for monitoring # * `:raise` - Raises an UnsafeRedirectError # # To allow any external redirects pass `allow_other_host: true`, though using a @@ -144,7 +144,7 @@ def allowed_redirect_hosts=(hosts) # config.action_controller.action_on_path_relative_redirect = :raise # # * `:log` - Logs a warning but allows the redirect - # * `:notify` - Sends an ActiveSupport notification but allows the redirect + # * `:notify` - Sends an Active Support notification but allows the redirect # (includes stack trace to help identify the source) # * `:raise` - Raises an UnsafeRedirectError def redirect_to(options = {}, response_options = {}) diff --git a/actionpack/lib/action_controller/structured_event_subscriber.rb b/actionpack/lib/action_controller/structured_event_subscriber.rb index 7bf6cf35f4c15..8bee0acffeb2f 100644 --- a/actionpack/lib/action_controller/structured_event_subscriber.rb +++ b/actionpack/lib/action_controller/structured_event_subscriber.rb @@ -46,10 +46,14 @@ def halted_callback(event) def rescue_from_callback(event) exception = event.payload[:exception] + + exception_backtrace = exception.backtrace&.first + exception_backtrace = exception_backtrace&.delete_prefix("#{Rails.root}/") if defined?(Rails.root) && Rails.root + emit_event("action_controller.rescue_from_handled", exception_class: exception.class.name, exception_message: exception.message, - exception_backtrace: exception.backtrace&.first&.delete_prefix("#{Rails.root}/") + exception_backtrace: ) end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 43acaa10d5111..a198528a6d4c4 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -676,7 +676,7 @@ def test_redirect_to_path_relative_url_starting_with_an_at_with_log def test_redirect_to_path_relative_url_starting_with_an_at_with_notify with_path_relative_redirect(:notify) do events = [] - ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |*args| + subscriber = ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |*args| events << ActiveSupport::Notifications::Event.new(*args) end @@ -692,14 +692,14 @@ def test_redirect_to_path_relative_url_starting_with_an_at_with_notify assert_kind_of Array, event.payload[:stack_trace] assert event.payload[:stack_trace].any? { |line| line.include?("redirect_to_path_relative_url_starting_with_an_at") } ensure - ActiveSupport::Notifications.unsubscribe("unsafe_redirect.action_controller") + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber end end def test_redirect_to_path_relative_url_with_notify with_path_relative_redirect(:notify) do events = [] - ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |*args| + subscriber = ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |*args| events << ActiveSupport::Notifications::Event.new(*args) end @@ -715,7 +715,7 @@ def test_redirect_to_path_relative_url_with_notify assert_kind_of Array, event.payload[:stack_trace] assert event.payload[:stack_trace].any? { |line| line.include?("redirect_to_path_relative_url") } ensure - ActiveSupport::Notifications.unsubscribe("unsafe_redirect.action_controller") + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber end end @@ -760,7 +760,7 @@ def test_redirect_to_absolute_url_does_not_log def test_redirect_to_absolute_url_does_not_notify with_path_relative_redirect(:notify) do events = [] - ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |*args| + subscriber = ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |*args| events << ActiveSupport::Notifications::Event.new(*args) end @@ -774,7 +774,7 @@ def test_redirect_to_absolute_url_does_not_notify assert_equal "http://test.host/things/stuff", redirect_to_url assert_empty events ensure - ActiveSupport::Notifications.unsubscribe("unsafe_redirect.action_controller") + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber end end @@ -808,7 +808,7 @@ def test_redirect_to_query_string_url_does_not_trigger_path_relative_warning_wit def test_redirect_to_query_string_url_does_not_trigger_path_relative_warning_with_notify with_path_relative_redirect(:notify) do events = [] - ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |*args| + subscriber = ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |*args| events << ActiveSupport::Notifications::Event.new(*args) end @@ -818,7 +818,7 @@ def test_redirect_to_query_string_url_does_not_trigger_path_relative_warning_wit assert_empty events.select { |e| e.payload[:message]&.include?("Path relative URL redirect detected") } ensure - ActiveSupport::Notifications.unsubscribe("unsafe_redirect.action_controller") + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber end end @@ -864,7 +864,7 @@ def test_redirect_to_external_with_action_on_open_redirect_log def test_redirect_to_external_with_action_on_open_redirect_notify with_action_on_open_redirect(:notify) do events = [] - ActiveSupport::Notifications.subscribe("open_redirect.action_controller") do |*args| + subscriber = ActiveSupport::Notifications.subscribe("open_redirect.action_controller") do |*args| events << ActiveSupport::Notifications::Event.new(*args) end @@ -878,7 +878,7 @@ def test_redirect_to_external_with_action_on_open_redirect_notify assert_kind_of ActionDispatch::Request, event.payload[:request] assert_kind_of Array, event.payload[:stack_trace] ensure - ActiveSupport::Notifications.unsubscribe("open_redirect.action_controller") + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber end end
← Back to Alerts View on GitHub →