CVE-2023-49090: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: CarrierWave is a solution for file uploads for Rails, Sinatra and other Ruby web frameworks. CarrierWave has a Content-Type allowlist bypass vulnerability, possibly leading to XSS. The validation in `allowlisted_content_type?` determines Content-Type permissions by performing a partial match. If the `content_type` argument of `allowlisted_content_type?` is passed a value crafted by the attacker, Content-Types not included in the `content_type_allowlist` will be allowed. This issue has been patched in versions 2.2.5 and 3.0.5.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-11-29T15:15Z
Repository: https://github.com/carrierwaveuploader/carrierwave
Language: Ruby
Files changed: lib/carrierwave/uploader/content_type_allowlist.rb, spec/uploader/content_type_allowlist_spec.rb
Commit date: 2023-11-29 13:15:26 +0900
Commit message: Fix Content-Type allowlist bypass vulnerability

Refs. https://github.com/carrierwaveuploader/carrierwave/security/advisories/GHSA-gxhx-g4fq-49hj

Diff: diff --git a/lib/carrierwave/uploader/content_type_allowlist.rb b/lib/carrierwave/uploader/content_type_allowlist.rb
index 1a1498ca..0a6d6994 100644
--- a/lib/carrierwave/uploader/content_type_allowlist.rb
+++ b/lib/carrierwave/uploader/content_type_allowlist.rb
@@ -51,11 +51,11 @@ module CarrierWave
       end
 
       def allowlisted_content_type?(allowlist, content_type)
         Array(allowlist).any? do |item|
           item = Regexp.quote(item) if item.class != Regexp
-          content_type =~ /#{item}/
+          content_type =~ /\A#{item}/
         end
       end
 
     end # ContentTypeAllowlist
   end # Uploader
diff --git a/spec/uploader/content_type_allowlist_spec.rb b/spec/uploader/content_type_allowlist_spec.rb
index f6b9baf2..e38def1c 100644
--- a/spec/uploader/content_type_allowlist_spec.rb
+++ b/spec/uploader/content_type_allowlist_spec.rb
@@ -74,10 +74,37 @@ describe CarrierWave::Uploader do
           allow(uploader).to receive(:content_type_allowlist).and_return(/image\//)
 
           expect { uploader.cache!(ruby_file) }.not_to raise_error
         end
       end
+
+      context "with a crafted content type" do
+        before do
+          allow(bork_file).to receive(:content_type).and_return('text/plain; image/png')
+          allow(uploader).to receive(:content_type_allowlist).and_return('image/png')
+        end
+
+        it "does not allow spoofing" do
+          expect { uploader.cache!(bork_file) }.to raise_error(CarrierWave::IntegrityError)
+        end
+      end
+
+      context "when the allowlist contains charset" do
+        before do
+          allow(uploader).to receive(:content_type_allowlist).and_return(%r{text/plain;\s*charset=utf-8})
+        end
+
+        it "accepts the content with allowed charset" do
+          allow(bork_file).to receive(:content_type).and_return('text/plain; charset=utf-8')
+          expect { uploader.cache!(bork_file) }.not_to raise_error
+        end
+
+        it "rejects the content without charset" do
+          allow(bork_file).to receive(:content_type).and_return('text/plain')
+          expect { uploader.cache!(bork_file) }.to raise_error(CarrierWave::IntegrityError)
+        end
+      end
     end
 
     context "when there is a whitelist" do
       it "uses the whitelist but shows deprecation" do
         allow(uploader).to receive(:content_type_whitelist).and_return(['image/gif'])

Vulnerable code:           content_type =~ /#{item}/
Fixed code:           content_type =~ /\A#{item}/

      context "with a crafted content type" do
        before do
          allow(bork_file).to receive(:content_type).and_return('text/plain; image/png')
          allow(uploader).to receive(:content_type_allowlist).and_return('image/png')
        end

        it "does not allow spoofing" do
          expect { uploader.cache!(bork_file) }.to raise_error(CarrierWave::IntegrityError)
        end
      end

      context "when the allowlist contains charset" do
        before do
          allow(uploader).to receive(:content_type_allowlist).and_return(%r{text/plain;\s*charset=utf-8})
        end

        it "accepts the content with allowed charset" do
          allow(bork_file).to receive(:content_type).and_return('text/plain; charset=utf-8')
          expect { uploader.cache!(bork_file) }.not_to raise_error
        end

        it "rejects the content without charset" do
          allow(bork_file).to receive(:content_type).and_return('text/plain')
          expect { uploader.cache!(bork_file) }.to raise_error(CarrierWave::IntegrityError)
        end
      end