From ed9175aa07f7c4fe59682a7b661d478411242a4a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 11 Feb 2022 13:09:30 +0100 Subject: [PATCH] ActionDispatch::Executor don't fully trust `body#close` Under certain circumstances, the middleware isn't informed that the response body has been fully closed which result in request state not being fully reset before the next request. [CVE-2022-23633] --- .../action_dispatch/middleware/executor.rb | 2 +- actionpack/test/dispatch/executor_test.rb | 21 ++++++++++++++ .../lib/active_support/execution_wrapper.rb | 29 ++++++++++--------- activesupport/lib/active_support/reloader.rb | 2 +- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/actionpack/lib/action_dispatch/middleware/executor.rb b/actionpack/lib/action_dispatch/middleware/executor.rb index 129b18d3d9..a32f916260 100644 --- a/actionpack/lib/action_dispatch/middleware/executor.rb +++ b/actionpack/lib/action_dispatch/middleware/executor.rb @@ -9,7 +9,7 @@ def initialize(app, executor) end def call(env) - state = @executor.run! + state = @executor.run!(reset: true) begin response = @app.call(env) returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! } diff --git a/actionpack/test/dispatch/executor_test.rb b/actionpack/test/dispatch/executor_test.rb index 8eb6450385..e19e3bea32 100644 --- a/actionpack/test/dispatch/executor_test.rb +++ b/actionpack/test/dispatch/executor_test.rb @@ -119,6 +119,27 @@ def test_callbacks_execute_in_shared_context assert !defined?(@in_shared_context) # it's not in the test itself end + def test_body_abandonned + total = 0 + ran = 0 + completed = 0 + + executor.to_run { total += 1; ran += 1 } + executor.to_complete { total += 1; completed += 1} + + stack = middleware(proc { [200, {}, "response"] }) + + requests_count = 5 + + requests_count.times do + stack.call({}) + end + + assert_equal (requests_count * 2) - 1, total + assert_equal requests_count, ran + assert_equal requests_count - 1, completed + end + private def call_and_return_body(&block) app = middleware(block || proc { [200, {}, "response"] }) diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb index f48c586cad..77f41d94a2 100644 --- a/activesupport/lib/active_support/execution_wrapper.rb +++ b/activesupport/lib/active_support/execution_wrapper.rb @@ -62,18 +62,21 @@ def self.register_hook(hook, outer: false) # after the work has been performed. # # Where possible, prefer +wrap+. - def self.run! - if active? - Null + def self.run!(reset: false) + if reset + lost_instance = active.delete(Thread.current) + lost_instance&.complete! else - new.tap do |instance| - success = nil - begin - instance.run! - success = true - ensure - instance.complete! unless success - end + return Null if active? + end + + new.tap do |instance| + success = nil + begin + instance.run! + success = true + ensure + instance.complete! unless success end end end @@ -102,11 +105,11 @@ def self.inherited(other) # :nodoc: self.active = Concurrent::Hash.new def self.active? # :nodoc: - @active[Thread.current] + @active.key?(Thread.current) end def run! # :nodoc: - self.class.active[Thread.current] = true + self.class.active[Thread.current] = self run_callbacks(:run) end diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index b26d9c3665..5acece49b7 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -59,7 +59,7 @@ def self.reload! prepare! end - def self.run! # :nodoc: + def self.run!(reset: false) # :nodoc: if check! super else -- 2.35.0