ActiveStorage direct upload fails to sign content-length header for S3 service
Medium
R
Ruby on Rails
Submitted None
Actions:
Reported by
travispew
Vulnerability Details
Technical details and impact analysis
When a user makes a direct upload using ActiveStorage, the browser makes a request to the DirectUploadsController containing the direct_upload parameters filename, content_type, byte_size, and checksum. These are used to generate a presigned url that is then passed back to the browser, allowing the user to upload directly to S3.
In particular, the byte_size parameter is intended to be encoded in the url for content-length, preventing the user from uploading a file of a different size. Although Rails does not currently provide any built in validations, developers have been encouraged to modify the controller or provide their own controller if they want to create a validation. For example, a developer might decide to prohibit uploads greater than 10MB in size.
in all current version of Rails with ActiveStorage and direct uploads `active_storage/lib/active_storage/service/s3_service.rb`, the code generates the presigned_url as follows:
```ruby
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
instrument :url, key: key do |payload|
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
content_type: content_type, content_length: content_length, content_md5: checksum
payload[:url] = generated_url
generated_url
end
end
```
However, the aws-sdk-s3 gem *silently blacklists* the "content-length" header:
https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-s3/lib/aws-sdk-s3/presigner.rb#L22
This issue is also raised here: https://github.com/aws/aws-sdk-ruby/issues/2098
As a result, the content-length header is never actually part of the presigned url. As a result, a malicious user can select a file of arbitrary size, tell the direct uploads controller that the file is a different size, and then proceed to upload the file, bypassing the intended protection of the signed url.
The solution is to add the whitelist_headers argument:
```ruby
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
instrument :url, key: key do |payload|
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
content_type: content_type, content_length: content_length, content_md5: checksum,
whitelist_headers: ['content-length']
payload[:url] = generated_url
generated_url
end
end
```
After this is added, the content-length will be included in the presigned url and the client will be unable to upload a file of arbitrary size.
## Impact
The attacker could upload a file of any size, unless the S3 service is configured separately to prevent this, whereas the developer believes they have protected themselves against this. This could allow an attacker to upload a very large file to S3, incurring additional costs to the website owner or causing other harm.
Report Details
Additional information and metadata
State
Closed
Substate
Resolved
Submitted
Weakness
Client-Side Enforcement of Server-Side Security