CVE-2024-29034: Insufficient Information (NVD-CWE-noinfo)
value: CarrierWave is a solution for file uploads for Rails, Sinatra and other Ruby web frameworks. The vulnerability CVE-2023-49090 wasn't fully addressed. This vulnerability is caused by the fact that when uploading to object storage, including Amazon S3, it is possible to set a Content-Type value that is interpreted by browsers to be different from what's allowed by `content_type_allowlist`, by providing multiple values separated by commas. This bypassed value can be used to cause XSS. Upgrade to 3.0.7 or 2.2.6.

CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-24T20:15Z
Repository: https://github.com/carrierwaveuploader/carrierwave
Language: Ruby
Files changed: lib/carrierwave/sanitized_file.rb, spec/sanitized_file_spec.rb, spec/uploader/content_type_allowlist_spec.rb
Commit date: 2024-03-20 14:10:19 +0900
Commit message: Fix Content-Type allowlist bypass vulnerability remained

Refs. https://github.com/carrierwaveuploader/carrierwave/security/advisories/GHSA-vfmv-jfc5-pjjw

Diff: diff --git a/lib/carrierwave/sanitized_file.rb b/lib/carrierwave/sanitized_file.rb
index 92211768..8fb04c56 100644
--- a/lib/carrierwave/sanitized_file.rb
+++ b/lib/carrierwave/sanitized_file.rb
@@ -305,11 +305,11 @@ module CarrierWave
     end
 
     def declared_content_type
       @declared_content_type ||
         if @file.respond_to?(:content_type) && @file.content_type
-          @file.content_type.to_s.chomp
+          Marcel::MimeType.for(declared_type: @file.content_type.to_s.chomp)
         end
     end
 
     # Guess content type from its file extension. Limit what to be returned to prevent spoofing.
     def guessed_safe_content_type
diff --git a/spec/sanitized_file_spec.rb b/spec/sanitized_file_spec.rb
index 5941846c..4087d1bc 100644
--- a/spec/sanitized_file_spec.rb
+++ b/spec/sanitized_file_spec.rb
@@ -324,10 +324,37 @@ describe CarrierWave::SanitizedFile do
     it "does not raise an error if the path is not present" do
       sanitized_file = CarrierWave::SanitizedFile.new(nil)
 
       expect { sanitized_file.content_type }.not_to raise_error
     end
+
+    it "uses the first one when multiple mime types are given using a semicolon" do
+      file = File.open(file_path("bork.txt"))
+      allow(file).to receive(:content_type) { 'image/png; text/html' }
+
+      sanitized_file = CarrierWave::SanitizedFile.new(file)
+
+      expect(sanitized_file.content_type).to eq("image/png")
+    end
+
+    it "uses the first one when multiple mime types are given using a comma" do
+      file = File.open(file_path("bork.txt"))
+      allow(file).to receive(:content_type) { 'image/png, text/html' }
+
+      sanitized_file = CarrierWave::SanitizedFile.new(file)
+
+      expect(sanitized_file.content_type).to eq("image/png")
+    end
+
+    it "drops content type parameters" do
+      file = File.open(file_path("bork.txt"))
+      allow(file).to receive(:content_type) { 'text/html; charset=utf-8' }
+
+      sanitized_file = CarrierWave::SanitizedFile.new(file)
+
+      expect(sanitized_file.content_type).to eq("text/html")
+    end
   end
 
   describe "#content_type=" do
     it "sets content_type" do
       sanitized_file = CarrierWave::SanitizedFile.new(content_type: "image/png")
diff --git a/spec/uploader/content_type_allowlist_spec.rb b/spec/uploader/content_type_allowlist_spec.rb
index e38def1c..fd25a489 100644
--- a/spec/uploader/content_type_allowlist_spec.rb
+++ b/spec/uploader/content_type_allowlist_spec.rb
@@ -85,26 +85,10 @@ describe CarrierWave::Uploader do
 
         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:           @file.content_type.to_s.chomp

      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
Fixed code:           Marcel::MimeType.for(declared_type: @file.content_type.to_s.chomp)

    it "uses the first one when multiple mime types are given using a semicolon" do
      file = File.open(file_path("bork.txt"))
      allow(file).to receive(:content_type) { 'image/png; text/html' }

      sanitized_file = CarrierWave::SanitizedFile.new(file)

      expect(sanitized_file.content_type).to eq("image/png")
    end

    it "uses the first one when multiple mime types are given using a comma" do
      file = File.open(file_path("bork.txt"))
      allow(file).to receive(:content_type) { 'image/png, text/html' }

      sanitized_file = CarrierWave::SanitizedFile.new(file)

      expect(sanitized_file.content_type).to eq("image/png")
    end

    it "drops content type parameters" do
      file = File.open(file_path("bork.txt"))
      allow(file).to receive(:content_type) { 'text/html; charset=utf-8' }

      sanitized_file = CarrierWave::SanitizedFile.new(file)

      expect(sanitized_file.content_type).to eq("text/html")
    end