VYPR
Moderate severityNVD Advisory· Published Mar 23, 2026· Updated Mar 24, 2026

Rails Active Storage has a possible DoS vulnerability when in proxy mode via Range requests

CVE-2026-33174

Description

Active Storage allows users to attach cloud and local files in Rails applications. Prior to versions 8.1.2.1, 8.0.4.1, and 7.2.3.1, when serving files through Active Storage's proxy delivery mode, the proxy controller loads the entire requested byte range into memory before sending it. A request with a large or unbounded Range header (e.g. bytes=0-) could cause the server to allocate memory proportional to the file size, possibly resulting in a DoS vulnerability through memory exhaustion. Versions 8.1.2.1, 8.0.4.1, and 7.2.3.1 contain a patch.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

In Active Storage proxy delivery mode, an unbounded Range header can cause server memory exhaustion via large byte-range loads, enabling DoS.

Vulnerability

Overview

CVE-2026-33174 is a denial-of-service (DoS) vulnerability in Ruby on Rails' Active Storage component, affecting versions prior to 8.1.2.1, 8.0.4.1, and 7.2.3.1. The flaw resides in the proxy delivery mode's controller, which loads the entire requested byte range into memory before serving the file [1]. If an attacker sends an HTTP request with a large or unbounded Range header (e.g., bytes=0-), the server may allocate memory proportional to the file size, potentially leading to rapid memory exhaustion [2].

Exploitation

Prerequisites

The attack requires network access to a Rails application using Active Storage's proxy mode. No authentication is needed if the file endpoint is publicly accessible. The attacker crafts a request with an oversized byte range, causing the Rails server to read and buffer the full (or a very large) portion of the requested blob into RAM [2][3]. Because the range is not validated before processing, even a single request can trigger excessive allocation.

Impact

Successful exploitation results in a denial of service due to memory exhaustion. The Rails server process may crash or become unresponsive, affecting all users of the application. The vulnerability can be triggered repeatedly, prolonging service disruption. The official advisory notes that the attack complexity is low, and no special privileges are required [2].

Mitigation

The Rails team has patched the issue in versions 8.1.2.1, 8.0.4.1, and 7.2.3.1. The fix introduces a configurable streaming_chunk_max_size (default 100 MB) and a ranges_valid? method that rejects byte ranges exceeding this limit [3][4]. Administrators are strongly advised to upgrade to one of the patched versions immediately. No workaround is available for unpatched installations.

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
activestorageRubyGems
>= 8.1.0.beta1, < 8.1.2.18.1.2.1
activestorageRubyGems
>= 8.0.0.beta1, < 8.0.4.18.0.4.1
activestorageRubyGems
< 7.2.3.17.2.3.1

Affected products

2

Patches

3
42012eaaa88d

Configurable maxmimum streaming chunk size

https://github.com/rails/railsGannon McGibbonDec 2, 2025via ghsa
5 files changed · +41 1
  • activestorage/app/controllers/concerns/active_storage/streaming.rb+7 1 modified
    @@ -14,7 +14,7 @@ module ActiveStorage::Streaming
         def send_blob_byte_range_data(blob, range_header, disposition: nil)
           ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
     
    -      return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
    +      return head(:range_not_satisfiable) unless ranges_valid?(ranges)
     
           if ranges.length == 1
             range = ranges.first
    @@ -51,6 +51,12 @@ def send_blob_byte_range_data(blob, range_header, disposition: nil)
           )
         end
     
    +    def ranges_valid?(ranges)
    +      return false if ranges.blank? || ranges.all?(&:blank?)
    +
    +      ranges.sum { |range| range.end - range.begin } < ActiveStorage.streaming_chunk_max_size
    +    end
    +
         # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
         # The content type and filename is set directly from the +blob+.
         def send_blob_stream(blob, disposition: nil) # :doc:
    
  • activestorage/CHANGELOG.md+8 0 modified
    @@ -1,3 +1,11 @@
    +*   Configurable maxmimum streaming chunk size
    +
    +    Makes sure that byte ranges for blobs don't exceed 100mb by default.
    +    Content ranges that are too big can result in denial of service.
    +
    +    *Gannon McGibbon*
    +
    +
     ## Rails 8.1.2 (January 08, 2026) ##
     
     *   Restore ADC when signing URLs with IAM for GCS
    
  • activestorage/lib/active_storage/engine.rb+1 0 modified
    @@ -149,6 +149,7 @@ class Engine < Rails::Engine # :nodoc:
             ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
             ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
             ActiveStorage.track_variants = app.config.active_storage.track_variants || false
    +        ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes
           end
         end
     
    
  • activestorage/lib/active_storage.rb+2 0 modified
    @@ -27,6 +27,7 @@
     require "active_support"
     require "active_support/rails"
     require "active_support/core_ext/numeric/time"
    +require "active_support/core_ext/numeric/bytes"
     
     require "active_storage/version"
     require "active_storage/deprecator"
    @@ -352,6 +353,7 @@ module ActiveStorage
       ]
       mattr_accessor :unsupported_image_processing_arguments
     
    +  mattr_accessor :streaming_chunk_max_size, default: 100.megabytes
       mattr_accessor :service_urls_expire_in, default: 5.minutes
       mattr_accessor :touch_attachment_records, default: true
       mattr_accessor :urls_expire_in
    
  • activestorage/test/controllers/blobs/proxy_controller_test.rb+23 0 modified
    @@ -96,6 +96,20 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes
         assert_response :range_not_satisfiable
       end
     
    +  test "Byte Range is too big" do
    +    with_streaming_chunk_max_size(1.kilobyte) do
    +      get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=0-" }
    +      assert_response :range_not_satisfiable
    +    end
    +  end
    +
    +  test "Byte Range is too big overall" do
    +    with_streaming_chunk_max_size(8.bytes) do
    +      get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=0-5,6-12" }
    +      assert_response :range_not_satisfiable
    +    end
    +  end
    +
       test "multiple Byte Ranges" do
         boundary = SecureRandom.hex
         SecureRandom.stub :hex, boundary do
    @@ -148,6 +162,15 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes
         assert_not_nil response.headers["Content-Length"], "Content-Length header should be included in proxy mode"
         assert_equal blob.byte_size.to_s, response.headers["Content-Length"], "Content-Length header should match blob size"
       end
    +
    +  private
    +    def with_streaming_chunk_max_size(size)
    +      old_size = ActiveStorage.streaming_chunk_max_size
    +      ActiveStorage.streaming_chunk_max_size = size
    +      yield
    +    ensure
    +      ActiveStorage.streaming_chunk_max_size = old_size
    +    end
     end
     
     class ActiveStorage::Blobs::ExpiringProxyControllerTest < ActionDispatch::IntegrationTest
    
8159a9c3de3f

Configurable maxmimum streaming chunk size

https://github.com/rails/railsGannon McGibbonDec 2, 2025via ghsa
5 files changed · +41 1
  • activestorage/app/controllers/concerns/active_storage/streaming.rb+7 1 modified
    @@ -14,7 +14,7 @@ module ActiveStorage::Streaming
         def send_blob_byte_range_data(blob, range_header, disposition: nil)
           ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
     
    -      return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
    +      return head(:range_not_satisfiable) unless ranges_valid?(ranges)
     
           if ranges.length == 1
             range = ranges.first
    @@ -51,6 +51,12 @@ def send_blob_byte_range_data(blob, range_header, disposition: nil)
           )
         end
     
    +    def ranges_valid?(ranges)
    +      return false if ranges.blank? || ranges.all?(&:blank?)
    +
    +      ranges.sum { |range| range.end - range.begin } < ActiveStorage.streaming_chunk_max_size
    +    end
    +
         # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
         # The content type and filename is set directly from the +blob+.
         def send_blob_stream(blob, disposition: nil) # :doc:
    
  • activestorage/CHANGELOG.md+8 0 modified
    @@ -1,3 +1,11 @@
    +*   Configurable maxmimum streaming chunk size
    +
    +    Makes sure that byte ranges for blobs don't exceed 100mb by default.
    +    Content ranges that are too big can result in denial of service.
    +
    +    *Gannon McGibbon*
    +
    +
     ## Rails 7.2.3 (October 28, 2025) ##
     
     *   Fix `config.active_storage.touch_attachment_records` to work with eager loading.
    
  • activestorage/lib/active_storage/engine.rb+1 0 modified
    @@ -122,6 +122,7 @@ class Engine < Rails::Engine # :nodoc:
             ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
             ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
             ActiveStorage.track_variants = app.config.active_storage.track_variants || false
    +        ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes
           end
         end
     
    
  • activestorage/lib/active_storage.rb+2 0 modified
    @@ -27,6 +27,7 @@
     require "active_support"
     require "active_support/rails"
     require "active_support/core_ext/numeric/time"
    +require "active_support/core_ext/numeric/bytes"
     
     require "active_storage/version"
     require "active_storage/deprecator"
    @@ -350,6 +351,7 @@ module ActiveStorage
       ]
       mattr_accessor :unsupported_image_processing_arguments
     
    +  mattr_accessor :streaming_chunk_max_size, default: 100.megabytes
       mattr_accessor :service_urls_expire_in, default: 5.minutes
       mattr_accessor :touch_attachment_records, default: true
       mattr_accessor :urls_expire_in
    
  • activestorage/test/controllers/blobs/proxy_controller_test.rb+23 0 modified
    @@ -70,6 +70,20 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes
         assert_response :range_not_satisfiable
       end
     
    +  test "Byte Range is too big" do
    +    with_streaming_chunk_max_size(1.kilobyte) do
    +      get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=0-" }
    +      assert_response :range_not_satisfiable
    +    end
    +  end
    +
    +  test "Byte Range is too big overall" do
    +    with_streaming_chunk_max_size(8.bytes) do
    +      get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=0-5,6-12" }
    +      assert_response :range_not_satisfiable
    +    end
    +  end
    +
       test "multiple Byte Ranges" do
         boundary = SecureRandom.hex
         SecureRandom.stub :hex, boundary do
    @@ -105,6 +119,15 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes
         request = ActionController::TestRequest.create({})
         assert_instance_of ActionController::Live::Response, ActiveStorage::Blobs::ProxyController.make_response!(request)
       end
    +
    +  private
    +    def with_streaming_chunk_max_size(size)
    +      old_size = ActiveStorage.streaming_chunk_max_size
    +      ActiveStorage.streaming_chunk_max_size = size
    +      yield
    +    ensure
    +      ActiveStorage.streaming_chunk_max_size = old_size
    +    end
     end
     
     class ActiveStorage::Blobs::ExpiringProxyControllerTest < ActionDispatch::IntegrationTest
    
2cd933c366b7

Configurable maxmimum streaming chunk size

https://github.com/rails/railsGannon McGibbonDec 2, 2025via ghsa
5 files changed · +41 1
  • activestorage/app/controllers/concerns/active_storage/streaming.rb+7 1 modified
    @@ -14,7 +14,7 @@ module ActiveStorage::Streaming
         def send_blob_byte_range_data(blob, range_header, disposition: nil)
           ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
     
    -      return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
    +      return head(:range_not_satisfiable) unless ranges_valid?(ranges)
     
           if ranges.length == 1
             range = ranges.first
    @@ -51,6 +51,12 @@ def send_blob_byte_range_data(blob, range_header, disposition: nil)
           )
         end
     
    +    def ranges_valid?(ranges)
    +      return false if ranges.blank? || ranges.all?(&:blank?)
    +
    +      ranges.sum { |range| range.end - range.begin } < ActiveStorage.streaming_chunk_max_size
    +    end
    +
         # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
         # The content type and filename is set directly from the +blob+.
         def send_blob_stream(blob, disposition: nil) # :doc:
    
  • activestorage/CHANGELOG.md+8 0 modified
    @@ -1,3 +1,11 @@
    +*   Configurable maxmimum streaming chunk size
    +
    +    Makes sure that byte ranges for blobs don't exceed 100mb by default.
    +    Content ranges that are too big can result in denial of service.
    +
    +    *Gannon McGibbon*
    +
    +
     ## Rails 8.0.4 (October 28, 2025) ##
     
     *   No changes.
    
  • activestorage/lib/active_storage/engine.rb+1 0 modified
    @@ -122,6 +122,7 @@ class Engine < Rails::Engine # :nodoc:
             ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
             ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
             ActiveStorage.track_variants = app.config.active_storage.track_variants || false
    +        ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes
           end
         end
     
    
  • activestorage/lib/active_storage.rb+2 0 modified
    @@ -27,6 +27,7 @@
     require "active_support"
     require "active_support/rails"
     require "active_support/core_ext/numeric/time"
    +require "active_support/core_ext/numeric/bytes"
     
     require "active_storage/version"
     require "active_storage/deprecator"
    @@ -350,6 +351,7 @@ module ActiveStorage
       ]
       mattr_accessor :unsupported_image_processing_arguments
     
    +  mattr_accessor :streaming_chunk_max_size, default: 100.megabytes
       mattr_accessor :service_urls_expire_in, default: 5.minutes
       mattr_accessor :touch_attachment_records, default: true
       mattr_accessor :urls_expire_in
    
  • activestorage/test/controllers/blobs/proxy_controller_test.rb+23 0 modified
    @@ -96,6 +96,20 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes
         assert_response :range_not_satisfiable
       end
     
    +  test "Byte Range is too big" do
    +    with_streaming_chunk_max_size(1.kilobyte) do
    +      get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=0-" }
    +      assert_response :range_not_satisfiable
    +    end
    +  end
    +
    +  test "Byte Range is too big overall" do
    +    with_streaming_chunk_max_size(8.bytes) do
    +      get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=0-5,6-12" }
    +      assert_response :range_not_satisfiable
    +    end
    +  end
    +
       test "multiple Byte Ranges" do
         boundary = SecureRandom.hex
         SecureRandom.stub :hex, boundary do
    @@ -131,6 +145,15 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes
         request = ActionController::TestRequest.create({})
         assert_instance_of ActionController::Live::Response, ActiveStorage::Blobs::ProxyController.make_response!(request)
       end
    +
    +  private
    +    def with_streaming_chunk_max_size(size)
    +      old_size = ActiveStorage.streaming_chunk_max_size
    +      ActiveStorage.streaming_chunk_max_size = size
    +      yield
    +    ensure
    +      ActiveStorage.streaming_chunk_max_size = old_size
    +    end
     end
     
     class ActiveStorage::Blobs::ExpiringProxyControllerTest < ActionDispatch::IntegrationTest
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

10

News mentions

0

No linked articles in our index yet.