CVE-2021-21305: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: CarrierWave is an open-source RubyGem which provides a simple and flexible way to upload files from Ruby applications. In CarrierWave before versions 1.3.2 and 2.1.1, there is a code injection vulnerability. The "#manipulate!" method inappropriately evals the content of mutation option(:read/:write), allowing attackers to craft a string that can be executed as a Ruby code. If an application developer supplies untrusted inputs to the option, it will lead to remote code execution(RCE). This is fixed in versions 1.3.2 and 2.1.1.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 8.8
Published: 2021-02-08T20:15Z
Repository: https://github.com/carrierwaveuploader/carrierwave
Language: Ruby
Files changed: lib/carrierwave/processing/rmagick.rb, spec/processing/rmagick_spec.rb
Commit date: 2021-02-08 13:42:28 +0900
Commit message: Fix Code Injection vulnerability in CarrierWave::RMagick

Refs. https://github.com/carrierwaveuploader/carrierwave/security/advisories/GHSA-cf3w-g86h-35x4

Diff: diff --git a/lib/carrierwave/processing/rmagick.rb b/lib/carrierwave/processing/rmagick.rb
index 45d57039..2c0786a3 100644
--- a/lib/carrierwave/processing/rmagick.rb
+++ b/lib/carrierwave/processing/rmagick.rb
@@ -376,13 +376,19 @@ module CarrierWave
 
   private
 
     def create_info_block(options)
       return nil unless options
-      assignments = options.map { |k, v| "img.#{k} = #{v}" }
-      code = "lambda { |img| " + assignments.join(";") + "}"
-      eval code
+      proc do |img|
+        options.each do |k, v|
+          if v.is_a?(String) && (matches = v.match(/^["'](.+)["']/))
+            ActiveSupport::Deprecation.warn "Passing quoted strings like #{v} to #manipulate! is deprecated, pass them without quoting."
+            v = matches[1]
+          end
+          img.public_send(:"#{k}=", v)
+        end
+      end
     end
 
     def destroy_image(image)
       image.try(:destroy!)
     end
diff --git a/spec/processing/rmagick_spec.rb b/spec/processing/rmagick_spec.rb
index 2c5d95fd..19037749 100644
--- a/spec/processing/rmagick_spec.rb
+++ b/spec/processing/rmagick_spec.rb
@@ -206,13 +206,45 @@ describe CarrierWave::RMagick, :rmagick => true do
       expect_any_instance_of(::Magick::Image::Info).to receive(:density=).with(10)
       expect_any_instance_of(::Magick::Image::Info).to receive(:size=).with("200x200")
 
       instance.manipulate! :read => {
           :density => 10,
-          :size => %{"200x200"}
+          :size => "200x200"
         }
     end
+
+    it 'shows deprecation but still accepts strings enclosed with double quotes' do
+      expect_any_instance_of(::Magick::Image::Info).to receive(:size=).once.with("200x200")
+      expect(ActiveSupport::Deprecation).to receive(:warn).with(any_args)
+      instance.manipulate! :read => {:size => %{"200x200"}}
+    end
+
+    it 'shows deprecation but still accepts strings enclosed with single quotes' do
+      expect_any_instance_of(::Magick::Image::Info).to receive(:size=).once.with("200x200")
+      expect(ActiveSupport::Deprecation).to receive(:warn).with(any_args)
+      instance.manipulate! :read => {:size => %{'200x200'}}
+    end
+
+    it 'does not allow arbitrary code execution' do
+      expect_any_instance_of(Kernel).not_to receive(:puts)
+      expect do
+        instance.manipulate! :read => {
+            :density => "1 }; raise; {"
+        }
+      end.to raise_error ArgumentError, /invalid density geometry/
+    end
+
+    it 'does not allow invocation of non-public methods' do
+      module Kernel
+        private def foo=(value); raise; end
+      end
+      expect do
+        instance.manipulate! :read => {
+            :foo => "1"
+        }
+      end.to raise_error NoMethodError, /private method `foo=' called/
+    end
   end
 
   describe "#width and #height" do
     it "returns the width and height of the image" do
       instance.resize_to_fill(200, 300)

Vulnerable code:       assignments = options.map { |k, v| "img.#{k} = #{v}" }
      code = "lambda { |img| " + assignments.join(";") + "}"
      eval code
          :size => %{"200x200"}
Fixed code:       proc do |img|
        options.each do |k, v|
          if v.is_a?(String) && (matches = v.match(/^["'](.+)["']/))
            ActiveSupport::Deprecation.warn "Passing quoted strings like #{v} to #manipulate! is deprecated, pass them without quoting."
            v = matches[1]
          end
          img.public_send(:"#{k}=", v)
        end
      end
          :size => "200x200"

    it 'shows deprecation but still accepts strings enclosed with double quotes' do
      expect_any_instance_of(::Magick::Image::Info).to receive(:size=).once.with("200x200")
      expect(ActiveSupport::Deprecation).to receive(:warn).with(any_args)
      instance.manipulate! :read => {:size => %{"200x200"}}
    end

    it 'shows deprecation but still accepts strings enclosed with single quotes' do
      expect_any_instance_of(::Magick::Image::Info).to receive(:size=).once.with("200x200")
      expect(ActiveSupport::Deprecation).to receive(:warn).with(any_args)
      instance.manipulate! :read => {:size => %{'200x200'}}
    end

    it 'does not allow arbitrary code execution' do
      expect_any_instance_of(Kernel).not_to receive(:puts)
      expect do
        instance.manipulate! :read => {
            :density => "1 }; raise; {"
        }
      end.to raise_error ArgumentError, /invalid density geometry/
    end

    it 'does not allow invocation of non-public methods' do
      module Kernel
        private def foo=(value); raise; end
      end
      expect do
        instance.manipulate! :read => {
            :foo => "1"
        }
      end.to raise_error NoMethodError, /private method `foo=' called/
    end