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

Rails Active Storage has possible content type bypass via metadata in direct uploads

CVE-2026-33173

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, DirectUploadsController accepts arbitrary metadata from the client and persists it on the blob. Because internal flags like identified and analyzed are stored in the same metadata hash, a direct-upload client can set these flags to skip MIME detection and analysis. This allows an attacker to upload arbitrary content while claiming a safe content_type, bypassing any validations that rely on Active Storage's automatic content type identification. 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.

CVE-2026-33173 allows an attacker to bypass MIME type detection in Active Storage by supplying metadata flags that mark a blob as already identified and analyzed.

Vulnerability

Overview

Active Storage’s DirectUploadsController accepts arbitrary metadata from the client and persists it on the blob. Internal flags such as identified and analyzed are stored in the same metadata hash, meaning a direct-upload client can set these flags to skip MIME detection and analysis. Consequently, an attacker can upload arbitrary content while claiming a safe content_type, bypassing any validations that rely on Active Storage's automatic content type identification [1][2].

Exploitation

Exploitation requires only network access to a Rails application that uses Active Storage with direct uploads enabled. No authentication is needed because the direct-upload endpoint is typically public. An attacker crafts a request to DirectUploadsController that includes metadata with identified and analyzed set to true, and any desired content_type. The server then skips its normal MIME detection, accepting the attacker-supplied values [2][3].

Impact

A successful attack allows arbitrary file uploads disguised as safe file types. This can lead to various downstream attacks, such as serving malicious content to users, overwriting existing blobs, or triggering further vulnerabilities if the application relies on the claimed MIME type for security decisions. The impact depends on the application's use of the uploaded file [1][2].

Mitigation

Patched versions 8.1.2.1, 8.0.4.1, and 7.2.3.1 filter user-supplied metadata in DirectUploadController, preventing internal flags from being overwritten. Users should upgrade immediately. No workarounds are documented [4].

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
8fcb934caadc

Active Storage: Filter user supplied metadata in DirectUploadController

https://github.com/rails/railsJean BoussierJan 7, 2026via ghsa
2 files changed · +24 1
  • activestorage/app/models/active_storage/blob.rb+15 0 modified
    @@ -20,6 +20,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
       MINIMUM_TOKEN_LENGTH = 28
     
       has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
    +
    +  # FIXME: these property should never have been stored in the metadata.
    +  # The blob table should be migrated to have dedicated columns for theses.
    +  PROTECTED_METADATA = %w(analyzed identified composed)
    +  private_constant :PROTECTED_METADATA
       store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
     
       class_attribute :services, default: {}
    @@ -104,6 +109,7 @@ def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: ni
         # Once the form using the direct upload is submitted, the blob can be associated with the right record using
         # the signed ID.
         def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
    +      metadata = filter_metadata(metadata)
           create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
         end
     
    @@ -151,6 +157,15 @@ def compose(blobs, key: nil, filename:, content_type: nil, metadata: nil)
             combined_blob.save!
           end
         end
    +
    +    private
    +      def filter_metadata(metadata)
    +        if metadata.is_a?(Hash)
    +          metadata.without(*PROTECTED_METADATA)
    +        else
    +          metadata
    +        end
    +      end
       end
     
       include Analyzable
    
  • activestorage/test/controllers/direct_uploads_controller_test.rb+9 1 modified
    @@ -145,8 +145,16 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
           "library_ID" => "12345"
         }
     
    +    protected_metadata = {
    +      "analyzed" => "yolo",
    +      "identified" => 42,
    +      "composed" => "maybe",
    +    }
    +
    +    all_metadata = metadata.merge(protected_metadata)
    +
         post rails_direct_uploads_url, params: { blob: {
    -      filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
    +      filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: all_metadata } }
     
         response.parsed_body.tap do |details|
           assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
    
707c0f1f41f0

Active Storage: Filter user supplied metadata in DirectUploadController

https://github.com/rails/railsJean BoussierJan 7, 2026via ghsa
2 files changed · +24 1
  • activestorage/app/models/active_storage/blob.rb+15 0 modified
    @@ -20,6 +20,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
       MINIMUM_TOKEN_LENGTH = 28
     
       has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
    +
    +  # FIXME: these property should never have been stored in the metadata.
    +  # The blob table should be migrated to have dedicated columns for theses.
    +  PROTECTED_METADATA = %w(analyzed identified composed)
    +  private_constant :PROTECTED_METADATA
       store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
     
       class_attribute :services, default: {}
    @@ -105,6 +110,7 @@ def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: ni
         # Once the form using the direct upload is submitted, the blob can be associated with the right record using
         # the signed ID.
         def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
    +      metadata = filter_metadata(metadata)
           create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
         end
     
    @@ -152,6 +158,15 @@ def compose(blobs, key: nil, filename:, content_type: nil, metadata: nil)
             combined_blob.save!
           end
         end
    +
    +    private
    +      def filter_metadata(metadata)
    +        if metadata.is_a?(Hash)
    +          metadata.without(*PROTECTED_METADATA)
    +        else
    +          metadata
    +        end
    +      end
       end
     
       include Analyzable
    
  • activestorage/test/controllers/direct_uploads_controller_test.rb+9 1 modified
    @@ -145,8 +145,16 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
           "library_ID" => "12345"
         }
     
    +    protected_metadata = {
    +      "analyzed" => "yolo",
    +      "identified" => 42,
    +      "composed" => "maybe",
    +    }
    +
    +    all_metadata = metadata.merge(protected_metadata)
    +
         post rails_direct_uploads_url, params: { blob: {
    -      filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
    +      filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: all_metadata } }
     
         response.parsed_body.tap do |details|
           assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
    
d9502f5214e2

Active Storage: Filter user supplied metadata in DirectUploadController

https://github.com/rails/railsJean BoussierJan 7, 2026via ghsa
2 files changed · +24 1
  • activestorage/app/models/active_storage/blob.rb+15 0 modified
    @@ -20,6 +20,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
       MINIMUM_TOKEN_LENGTH = 28
     
       has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
    +
    +  # FIXME: these property should never have been stored in the metadata.
    +  # The blob table should be migrated to have dedicated columns for theses.
    +  PROTECTED_METADATA = %w(analyzed identified composed)
    +  private_constant :PROTECTED_METADATA
       store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
     
       class_attribute :services, default: {}
    @@ -104,6 +109,7 @@ def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: ni
         # Once the form using the direct upload is submitted, the blob can be associated with the right record using
         # the signed ID.
         def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
    +      metadata = filter_metadata(metadata)
           create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
         end
     
    @@ -151,6 +157,15 @@ def compose(blobs, key: nil, filename:, content_type: nil, metadata: nil)
             combined_blob.save!
           end
         end
    +
    +    private
    +      def filter_metadata(metadata)
    +        if metadata.is_a?(Hash)
    +          metadata.without(*PROTECTED_METADATA)
    +        else
    +          metadata
    +        end
    +      end
       end
     
       include Analyzable
    
  • activestorage/test/controllers/direct_uploads_controller_test.rb+9 1 modified
    @@ -103,8 +103,16 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
           "library_ID" => "12345"
         }
     
    +    protected_metadata = {
    +      "analyzed" => "yolo",
    +      "identified" => 42,
    +      "composed" => "maybe",
    +    }
    +
    +    all_metadata = metadata.merge(protected_metadata)
    +
         post rails_direct_uploads_url, params: { blob: {
    -      filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata } }
    +      filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: all_metadata } }
     
         response.parsed_body.tap do |details|
           assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

10

News mentions

0

No linked articles in our index yet.