XSS (development/debug exception page)

HIGH
rails/rails
Commit: 4df808965b4a
Affected: Rails 8.1.x releases prior to 8.1.3 (development error pages)
2026-04-05 12:36 UTC

Description

This commit fixes an XSS in Rails' development debug exception page by escaping user-controlled exception messages when copied to the clipboard. Previously the exception message could be output with raw HTML inside a script tag used for the copy-to-clipboard feature, allowing HTML content to be injected if the message contained HTML. The patch replaces raw with default ERB escaping for the copied message and adds a test asserting escaping of the message (e.g., turning <script> into &lt;script&gt;). This mitigates a development-only XSS risk (CVE-2026-33167).

Proof of Concept

Pre-fix exploit flow (illustrative): 1) Trigger an exception with HTML in the message, e.g. access /xss_error which returns 'x</script><script>alert(1)</script>'. 2) Copy the exception message to the clipboard via the development error page's copy-to-clipboard control. 3) On a separate HTML page that unsafely inserts clipboard content into the DOM using innerHTML, paste/read the clipboard content to see the injected HTML execute: Attacker page example (dangerous HTML, test only): <!DOCTYPE html> <html> <body> <div id="out"></div> <script> // Attacker-provided clipboard content derived from the Rails dev page var payload = 'x</script><script>alert(1)</script>'; document.getElementById('out').innerHTML = payload; </script> </body> </html> This would result in an alert(1) in a browser if the content is inserted via innerHTML without escaping. With the fix, the copied content is escaped (e.g., x&lt;/script&gt;&lt;script&gt;alert(1)&lt;/script&gt;), which when inserted into the DOM would render as text rather than executing scripts, preventing the XSS.

Commit Details

Author: John Hawthorn

Date: 2026-03-17 17:16 UTC

Message:

Fix XSS in debug exceptions copy-to-clipboard The exception message was output with `raw` inside a <script> tag. If an attacker is able to trigger an exception message containing HTML it would be rendered to the page. This affects development error page and in most cases is not reachable in production. Use default ERB escaping instead of `raw` to ensure the message is HTML-escaped. [CVE-2026-33167] [GHSA-pgm4-439c-5jp6]

Triage Assessment

Vulnerability Type: XSS

Confidence: HIGH

Reasoning:

Commit changes escape user-controlled exception messages before copying to clipboard in the debug exception page. Replacing raw output with default escaping prevents HTML in exception messages from being interpreted or injected, addressing an XSS risk in development error pages (CVE-2026-33167).

Verification Assessment

Vulnerability Type: XSS (development/debug exception page)

Confidence: HIGH

Affected Versions: Rails 8.1.x releases prior to 8.1.3 (development error pages)

Code Diff

diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb index 3a84601d25518..6066ecd6d9510 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb @@ -345,7 +345,7 @@ <body> <%= yield %> - <script type="text/plain" id="exception-message-for-copy"><%= raw @exception_message_for_copy %></script> + <script type="text/plain" id="exception-message-for-copy"><%= @exception_message_for_copy %></script> </body> </html> diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index d9f109c4f5033..94c7911c4c480 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -128,6 +128,8 @@ def call(env) rescue Exception raise ActionView::Template::Error.new(template) end + when "/xss_error" + raise "x</script><script>alert(1)</script>" else raise "puke!" end @@ -978,6 +980,16 @@ def self.build_app(app, *args) assert_select "#container code", /undefined local variable or method ['`]string”'/ end + test "exception message is escaped in copy-to-clipboard script tag" do + @app = DevelopmentApp + + get "/xss_error", headers: { "action_dispatch.show_exceptions" => :all } + assert_response 500 + + assert_no_match "<script>alert(1)</script>", body + assert_match "&lt;script&gt;alert(1)&lt;/script&gt;", body + end + test "includes copy button in error pages" do @app = DevelopmentApp @@ -998,7 +1010,6 @@ def self.build_app(app, *args) assert_response 500 assert_no_match %r{<button}, body - assert_no_match %r{<script}, body end test "exception message includes causes for nested exceptions" do
← Back to Alerts View on GitHub →