Rails Active Storage has possible glob injection in its DiskService
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.
| Package | Affected versions | Patched versions |
|---|---|---|
activestorageRubyGems | >= 8.1.0.beta1, < 8.1.2.1 | 8.1.2.1 |
activestorageRubyGems | >= 8.0.0.beta1, < 8.0.4.1 | 8.0.4.1 |
activestorageRubyGems | < 7.2.3.1 | 7.2.3.1 |
Affected products
2- Range: < 7.2.3.1, < 8.0.4.1, < 8.1.2.1
- rails/activestoragev5Range: >= 8.1.0.beta1, < 8.1.2.1
Patches
3955284d26e46Prevent glob injection in ActiveStorage DiskService#delete_prefixed
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
8c9676b80382Prevent glob injection in ActiveStorage DiskService#delete_prefixed
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
fa1907354636Prevent glob injection in ActiveStorage DiskService#delete_prefixed
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- github.com/advisories/GHSA-73f9-jhhh-hr5mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33202ghsaADVISORY
- github.com/rails/rails/commit/8c9676b803820110548cdb7523800db43bc6874cghsax_refsource_MISCWEB
- github.com/rails/rails/commit/955284d26e469a9c026a4eee5b21f0414ab0bccfghsax_refsource_MISCWEB
- github.com/rails/rails/commit/fa19073546360856e9f4dab221fc2c5d73a45e82ghsax_refsource_MISCWEB
- github.com/rails/rails/releases/tag/v7.2.3.1ghsax_refsource_MISCWEB
- github.com/rails/rails/releases/tag/v8.0.4.1ghsax_refsource_MISCWEB
- github.com/rails/rails/releases/tag/v8.1.2.1ghsax_refsource_MISCWEB
- github.com/rails/rails/security/advisories/GHSA-73f9-jhhh-hr5mghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/activestorage/CVE-2026-33202.ymlghsaWEB
News mentions
0No linked articles in our index yet.