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

Rails Active Storage has possible glob injection in its DiskService

CVE-2026-33202

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, Active Storage's DiskService#delete_prefixed passes blob keys directly to Dir.glob without escaping glob metacharacters. If a blob key contains attacker-controlled input or custom-generated keys with glob metacharacters, it may be possible to delete unintended files from the storage directory. 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.

Active Storage DiskService#delete_prefixed passes blob keys to Dir.glob without escaping metacharacters, allowing unintended file deletion.

Vulnerability

Overview

CVE-2026-33202 is a glob injection vulnerability in Active Storage, the file attachment framework for Ruby on Rails. The DiskService#delete_prefixed method passes blob keys directly to Dir.glob without escaping glob metacharacters such as *, ?, [, ], {, }, and \. This allows an attacker who can control or influence blob key values to delete unintended files from the storage directory [1][2].

Exploitation

An attacker must be able to supply or generate blob keys containing glob metacharacters. This could occur if blob keys derived from user input, such as filenames or custom key generation logic. By crafting a key like prefix[abc]/file, the Dir.glob call interprets the brackets as a character class, potentially matching and deleting files outside the intended prefix scope. The attack requires no authentication beyond the ability to trigger the delete_prefixed operation, which may be exposed through application features like file cleanup or versioning [3][4].

Impact

Successful exploitation allows an attacker to delete arbitrary files from the storage directory, leading to data loss or denial of service. The impact is limited to files stored by Active Storage on the disk service; other storage services (e.g., S3) are not affected. The vulnerability does not allow reading or modifying files, only deletion [1][2].

Mitigation

Patched versions 8.1.2.1, 8.0.4.1, and 7.2.3.1 escape glob metacharacters in blob keys before passing them to Dir.glob. The fix introduces an escape_glob_metacharacters method that backslash-escapes each metacharacter. Users should upgrade to the latest patched version or apply the relevant commit [3][4]. No workaround is available for unpatched versions.

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
955284d26e46

Prevent glob injection in ActiveStorage DiskService#delete_prefixed

https://github.com/rails/railsMike DalessioMar 13, 2026via ghsa
3 files changed · +70 1
  • activestorage/CHANGELOG.md+10 0 modified
    @@ -18,6 +18,16 @@
     
         *Mike Dalessio*
     
    +*   Prevent glob injection in `DiskService#delete_prefixed`.
    +
    +    Escape glob metacharacters in the resolved path before passing to `Dir.glob`.
    +
    +    Note that this change breaks any existing code that is relying on `delete_prefixed` to expand
    +    glob metacharacters. This change presumes that is unintended behavior (as other storage services
    +    do not respect these metacharacters).
    +
    +    *Mike Dalessio*
    +
     
     ## Rails 8.0.4 (October 28, 2025) ##
     
    
  • activestorage/lib/active_storage/service/disk_service.rb+14 1 modified
    @@ -60,7 +60,16 @@ def delete(key)
     
         def delete_prefixed(prefix)
           instrument :delete_prefixed, prefix: prefix do
    -        Dir.glob(path_for("#{prefix}*")).each do |path|
    +        prefix_path = path_for(prefix)
    +
    +        # File.expand_path (called within path_for) strips trailing slashes.
    +        # Restore trailing separator if the original prefix had one, so that
    +        # the glob "prefix/*" matches files inside the directory, not siblings
    +        # whose names start with the prefix string.
    +        prefix_path += "/" if prefix.end_with?("/")
    +
    +        escaped = escape_glob_metacharacters(prefix_path)
    +        Dir.glob("#{escaped}*").each do |path|
               FileUtils.rm_rf(path)
             end
           end
    @@ -187,6 +196,10 @@ def folder_for(key)
             [ key[0..1], key[2..3] ].join("/")
           end
     
    +      def escape_glob_metacharacters(path)
    +        path.gsub(/[\[\]*?{}\\]/) { |c| "\\#{c}" }
    +      end
    +
           def make_path_for(key)
             path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
           end
    
  • activestorage/test/service/disk_service_test.rb+46 0 modified
    @@ -161,6 +161,52 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
         end
       end
     
    +  test "path_for escapes all glob metacharacters" do
    +    assert_equal "\\[", @service.send(:escape_glob_metacharacters, "[")
    +    assert_equal "\\]", @service.send(:escape_glob_metacharacters, "]")
    +    assert_equal "\\*", @service.send(:escape_glob_metacharacters, "*")
    +    assert_equal "\\?", @service.send(:escape_glob_metacharacters, "?")
    +    assert_equal "\\{", @service.send(:escape_glob_metacharacters, "{")
    +    assert_equal "\\}", @service.send(:escape_glob_metacharacters, "}")
    +    assert_equal "\\\\", @service.send(:escape_glob_metacharacters, "\\")
    +    assert_equal "hello", @service.send(:escape_glob_metacharacters, "hello")
    +    assert_equal "/path/to/\\[brackets\\]/file", @service.send(:escape_glob_metacharacters, "/path/to/[brackets]/file")
    +  end
    +
    +  test "delete_prefixed with glob metacharacters only deletes matching files" do
    +    base_key = SecureRandom.base58(24)
    +    bracket_key = "#{base_key}[1]/file"
    +    plain_key = "#{base_key}1/file"
    +
    +    @service.upload(bracket_key, StringIO.new("bracket"))
    +    @service.upload(plain_key, StringIO.new("plain"))
    +
    +    @service.delete_prefixed("#{base_key}[1]/")
    +
    +    assert @service.exist?(plain_key), "file should not be deleted"
    +    assert_not @service.exist?(bracket_key), "file should be deleted"
    +  ensure
    +    @service.delete(bracket_key) rescue nil
    +    @service.delete(plain_key) rescue nil
    +  end
    +
    +  test "delete_prefixed with trailing slash only deletes files inside the directory" do
    +    base_key = SecureRandom.base58(24)
    +    inside_key = "#{base_key}/file"
    +    sibling_key = "#{base_key}_sibling"
    +
    +    @service.upload(inside_key, StringIO.new("inside"))
    +    @service.upload(sibling_key, StringIO.new("sibling"))
    +
    +    @service.delete_prefixed("#{base_key}/")
    +
    +    assert @service.exist?(sibling_key), "sibling file should not be deleted"
    +    assert_not @service.exist?(inside_key), "file inside directory should be deleted"
    +  ensure
    +    @service.delete(inside_key) rescue nil
    +    @service.delete(sibling_key) rescue nil
    +  end
    +
       test "can change root" do
         tmp_path_2 = File.join(Dir.tmpdir, "active_storage_2")
         @service.root = tmp_path_2
    
8c9676b80382

Prevent glob injection in ActiveStorage DiskService#delete_prefixed

https://github.com/rails/railsMike DalessioMar 13, 2026via ghsa
3 files changed · +71 1
  • activestorage/CHANGELOG.md+11 0 modified
    @@ -19,6 +19,17 @@
     
         *Mike Dalessio*
     
    +*   Prevent glob injection in `DiskService#delete_prefixed`.
    +
    +    Escape glob metacharacters in the resolved path before passing to `Dir.glob`.
    +
    +    Note that this change breaks any existing code that is relying on `delete_prefixed` to expand
    +    glob metacharacters. This change presumes that is unintended behavior (as other storage services
    +    do not respect these metacharacters).
    +
    +    *Mike Dalessio*
    +
    +
     ## Rails 8.1.2 (January 08, 2026) ##
     
     *   Restore ADC when signing URLs with IAM for GCS
    
  • activestorage/lib/active_storage/service/disk_service.rb+14 1 modified
    @@ -60,7 +60,16 @@ def delete(key)
     
         def delete_prefixed(prefix)
           instrument :delete_prefixed, prefix: prefix do
    -        Dir.glob(path_for("#{prefix}*")).each do |path|
    +        prefix_path = path_for(prefix)
    +
    +        # File.expand_path (called within path_for) strips trailing slashes.
    +        # Restore trailing separator if the original prefix had one, so that
    +        # the glob "prefix/*" matches files inside the directory, not siblings
    +        # whose names start with the prefix string.
    +        prefix_path += "/" if prefix.end_with?("/")
    +
    +        escaped = escape_glob_metacharacters(prefix_path)
    +        Dir.glob("#{escaped}*").each do |path|
               FileUtils.rm_rf(path)
             end
           end
    @@ -187,6 +196,10 @@ def folder_for(key)
             [ key[0..1], key[2..3] ].join("/")
           end
     
    +      def escape_glob_metacharacters(path)
    +        path.gsub(/[\[\]*?{}\\]/) { |c| "\\#{c}" }
    +      end
    +
           def make_path_for(key)
             path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
           end
    
  • activestorage/test/service/disk_service_test.rb+46 0 modified
    @@ -161,6 +161,52 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
         end
       end
     
    +  test "path_for escapes all glob metacharacters" do
    +    assert_equal "\\[", @service.send(:escape_glob_metacharacters, "[")
    +    assert_equal "\\]", @service.send(:escape_glob_metacharacters, "]")
    +    assert_equal "\\*", @service.send(:escape_glob_metacharacters, "*")
    +    assert_equal "\\?", @service.send(:escape_glob_metacharacters, "?")
    +    assert_equal "\\{", @service.send(:escape_glob_metacharacters, "{")
    +    assert_equal "\\}", @service.send(:escape_glob_metacharacters, "}")
    +    assert_equal "\\\\", @service.send(:escape_glob_metacharacters, "\\")
    +    assert_equal "hello", @service.send(:escape_glob_metacharacters, "hello")
    +    assert_equal "/path/to/\\[brackets\\]/file", @service.send(:escape_glob_metacharacters, "/path/to/[brackets]/file")
    +  end
    +
    +  test "delete_prefixed with glob metacharacters only deletes matching files" do
    +    base_key = SecureRandom.base58(24)
    +    bracket_key = "#{base_key}[1]/file"
    +    plain_key = "#{base_key}1/file"
    +
    +    @service.upload(bracket_key, StringIO.new("bracket"))
    +    @service.upload(plain_key, StringIO.new("plain"))
    +
    +    @service.delete_prefixed("#{base_key}[1]/")
    +
    +    assert @service.exist?(plain_key), "file should not be deleted"
    +    assert_not @service.exist?(bracket_key), "file should be deleted"
    +  ensure
    +    @service.delete(bracket_key) rescue nil
    +    @service.delete(plain_key) rescue nil
    +  end
    +
    +  test "delete_prefixed with trailing slash only deletes files inside the directory" do
    +    base_key = SecureRandom.base58(24)
    +    inside_key = "#{base_key}/file"
    +    sibling_key = "#{base_key}_sibling"
    +
    +    @service.upload(inside_key, StringIO.new("inside"))
    +    @service.upload(sibling_key, StringIO.new("sibling"))
    +
    +    @service.delete_prefixed("#{base_key}/")
    +
    +    assert @service.exist?(sibling_key), "sibling file should not be deleted"
    +    assert_not @service.exist?(inside_key), "file inside directory should be deleted"
    +  ensure
    +    @service.delete(inside_key) rescue nil
    +    @service.delete(sibling_key) rescue nil
    +  end
    +
       test "can change root" do
         tmp_path_2 = File.join(Dir.tmpdir, "active_storage_2")
         @service.root = tmp_path_2
    
fa1907354636

Prevent glob injection in ActiveStorage DiskService#delete_prefixed

https://github.com/rails/railsMike DalessioMar 13, 2026via ghsa
3 files changed · +70 1
  • activestorage/CHANGELOG.md+10 0 modified
    @@ -18,6 +18,16 @@
     
         *Mike Dalessio*
     
    +*   Prevent glob injection in `DiskService#delete_prefixed`.
    +
    +    Escape glob metacharacters in the resolved path before passing to `Dir.glob`.
    +
    +    Note that this change breaks any existing code that is relying on `delete_prefixed` to expand
    +    glob metacharacters. This change presumes that is unintended behavior (as other storage services
    +    do not respect these metacharacters).
    +
    +    *Mike Dalessio*
    +
     
     ## Rails 7.2.3 (October 28, 2025) ##
     
    
  • activestorage/lib/active_storage/service/disk_service.rb+14 1 modified
    @@ -60,7 +60,16 @@ def delete(key)
     
         def delete_prefixed(prefix)
           instrument :delete_prefixed, prefix: prefix do
    -        Dir.glob(path_for("#{prefix}*")).each do |path|
    +        prefix_path = path_for(prefix)
    +
    +        # File.expand_path (called within path_for) strips trailing slashes.
    +        # Restore trailing separator if the original prefix had one, so that
    +        # the glob "prefix/*" matches files inside the directory, not siblings
    +        # whose names start with the prefix string.
    +        prefix_path += "/" if prefix.end_with?("/")
    +
    +        escaped = escape_glob_metacharacters(prefix_path)
    +        Dir.glob("#{escaped}*").each do |path|
               FileUtils.rm_rf(path)
             end
           end
    @@ -187,6 +196,10 @@ def folder_for(key)
             [ key[0..1], key[2..3] ].join("/")
           end
     
    +      def escape_glob_metacharacters(path)
    +        path.gsub(/[\[\]*?{}\\]/) { |c| "\\#{c}" }
    +      end
    +
           def make_path_for(key)
             path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
           end
    
  • activestorage/test/service/disk_service_test.rb+46 0 modified
    @@ -161,6 +161,52 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
         end
       end
     
    +  test "path_for escapes all glob metacharacters" do
    +    assert_equal "\\[", @service.send(:escape_glob_metacharacters, "[")
    +    assert_equal "\\]", @service.send(:escape_glob_metacharacters, "]")
    +    assert_equal "\\*", @service.send(:escape_glob_metacharacters, "*")
    +    assert_equal "\\?", @service.send(:escape_glob_metacharacters, "?")
    +    assert_equal "\\{", @service.send(:escape_glob_metacharacters, "{")
    +    assert_equal "\\}", @service.send(:escape_glob_metacharacters, "}")
    +    assert_equal "\\\\", @service.send(:escape_glob_metacharacters, "\\")
    +    assert_equal "hello", @service.send(:escape_glob_metacharacters, "hello")
    +    assert_equal "/path/to/\\[brackets\\]/file", @service.send(:escape_glob_metacharacters, "/path/to/[brackets]/file")
    +  end
    +
    +  test "delete_prefixed with glob metacharacters only deletes matching files" do
    +    base_key = SecureRandom.base58(24)
    +    bracket_key = "#{base_key}[1]/file"
    +    plain_key = "#{base_key}1/file"
    +
    +    @service.upload(bracket_key, StringIO.new("bracket"))
    +    @service.upload(plain_key, StringIO.new("plain"))
    +
    +    @service.delete_prefixed("#{base_key}[1]/")
    +
    +    assert @service.exist?(plain_key), "file should not be deleted"
    +    assert_not @service.exist?(bracket_key), "file should be deleted"
    +  ensure
    +    @service.delete(bracket_key) rescue nil
    +    @service.delete(plain_key) rescue nil
    +  end
    +
    +  test "delete_prefixed with trailing slash only deletes files inside the directory" do
    +    base_key = SecureRandom.base58(24)
    +    inside_key = "#{base_key}/file"
    +    sibling_key = "#{base_key}_sibling"
    +
    +    @service.upload(inside_key, StringIO.new("inside"))
    +    @service.upload(sibling_key, StringIO.new("sibling"))
    +
    +    @service.delete_prefixed("#{base_key}/")
    +
    +    assert @service.exist?(sibling_key), "sibling file should not be deleted"
    +    assert_not @service.exist?(inside_key), "file inside directory should be deleted"
    +  ensure
    +    @service.delete(inside_key) rescue nil
    +    @service.delete(sibling_key) rescue nil
    +  end
    +
       test "can change root" do
         tmp_path_2 = File.join(Dir.tmpdir, "active_storage_2")
         @service.root = tmp_path_2
    

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.