VYPR
High severityNVD Advisory· Published Feb 2, 2021· Updated Aug 3, 2024

Command Injection Vulnerability in Mechanize

CVE-2021-21289

Description

Mechanize is an open-source ruby library that makes automated web interaction easy. In Mechanize from version 2.0.0 and before version 2.7.7 there is a command injection vulnerability. Affected versions of mechanize allow for OS commands to be injected using several classes' methods which implicitly use Ruby's Kernel.open method. Exploitation is possible only if untrusted input is used as a local filename and passed to any of these calls: Mechanize::CookieJar#load, Mechanize::CookieJar#save_as, Mechanize#download, Mechanize::Download#save, Mechanize::File#save, and Mechanize::FileResponse#read_body. This is fixed in version 2.7.7.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Mechanize library before 2.7.7 allows OS command injection via Kernel.open when untrusted filenames are passed to certain methods.

Vulnerability

Overview

Mechanize versions 2.0.0 to 2.7.6 contain a command injection vulnerability due to the implicit use of Ruby's Kernel.open method in several class methods. Kernel.open can execute arbitrary OS commands if a filename argument starts with a pipe character (|). Affected methods include Mechanize::CookieJar#load, Mechanize::CookieJar#save_as, Mechanize#download, Mechanize::Download#save, Mechanize::File#save, and Mechanize::FileResponse#read_body. [1][2]

Exploitation

Prerequisites

Exploitation is possible only if an attacker can control the local filename argument passed to any of these methods. This typically requires the application to accept untrusted input as a filename (e.g., from user uploads, cookies, or file paths). No authentication is strictly required, but the attacker must be able to influence the filename parameter. The vulnerability exists because the affected methods use Kernel.open instead of File.open to handle file operations. [2]

Impact

If successfully exploited, an attacker can execute arbitrary OS commands with the privileges of the Ruby application. This could lead to full system compromise, data exfiltration, or further lateral movement within the network. The severity is reflected in the CVSS score, indicating a high-risk vulnerability. [1]

Mitigation

The vulnerability is fixed in Mechanize version 2.7.7. The commit [2] addresses the issue by replacing Kernel.open with File.open in the affected methods, preventing command injection. Users are strongly advised to upgrade to version 2.7.7 or later. No workarounds are documented; the only complete mitigation is updating the gem. The Rubysec advisory database also confirms the fix. [3]

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mechanizeRubyGems
>= 2.0.0, < 2.7.72.7.7

Affected products

2
  • ghsa-coords
    Range: >= 2.0.0, < 2.7.7
  • sparklemotion/mechanizev5
    Range: >= 2.0, < 2.7.7

Patches

1
66a6a1bfa653

Merge pull request #548 from kyoshidajp/fix_command_injection

https://github.com/sparklemotion/mechanizeMike DalessioFeb 1, 2021via ghsa
14 files changed · +108 18
  • CHANGELOG.rdoc+16 0 modified
    @@ -2,6 +2,22 @@
     
     === Unreleased
     
    +* Security
    +
    +  Mechanize `>= v2.0`, `< v2.7.7` allows for OS commands to be injected into several classes'
    +  methods via implicit use of Ruby's `Kernel.open` method. Exploitation is possible only if
    +  untrusted input is used as a local filename and passed to any of these calls:
    +
    +  - `Mechanize::CookieJar#load`: since v2.0 (see 208e3ed)
    +  - `Mechanize::CookieJar#save_as`: since v2.0 (see 5b776a4)
    +  - `Mechanize#download`: since v2.2 (see dc91667)
    +  - `Mechanize::Download#save` and `#save!` since v2.1 (see 98b2f51, bd62ff0)
    +  - `Mechanize::File#save` and `#save_as`: since v2.1 (see 2bf7519)
    +  - `Mechanize::FileResponse#read_body`: since v2.0 (see 01039f5)
    +
    +  See https://github.com/sparklemotion/mechanize/security/advisories/GHSA-qrqm-fpv6-6r8g for more
    +  information.
    +
     * New Features
       * Support for Ruby 3.0 by adding `webrick` as a runtime dependency. (#557) @pvalena
     
    
  • lib/mechanize/cookie_jar.rb+2 2 modified
    @@ -65,7 +65,7 @@ def dump_cookiestxt(io)
       class CookieJar < ::HTTP::CookieJar
         def save(output, *options)
           output.respond_to?(:write) or
    -        return open(output, 'w') { |io| save(io, *options) }
    +        return ::File.open(output, 'w') { |io| save(io, *options) }
     
           opthash = {
             :format => :yaml,
    @@ -119,7 +119,7 @@ def save(output, *options)
     
         def load(input, *options)
           input.respond_to?(:write) or
    -        return open(input, 'r') { |io| load(io, *options) }
    +        return ::File.open(input, 'r') { |io| load(io, *options) }
     
           opthash = {
             :format => :yaml,
    
  • lib/mechanize/download.rb+1 1 modified
    @@ -71,7 +71,7 @@ def save! filename = nil
         dirname = File.dirname filename
         FileUtils.mkdir_p dirname
     
    -    open filename, 'wb' do |io|
    +    ::File.open(filename, 'wb')do |io|
           until @body_io.eof? do
             io.write @body_io.read 16384
           end
    
  • lib/mechanize/file.rb+1 1 modified
    @@ -82,7 +82,7 @@ def save! filename = nil
         dirname = File.dirname filename
         FileUtils.mkdir_p dirname
     
    -    open filename, 'wb' do |f|
    +    ::File.open(filename, 'wb')do |f|
           f.write body
         end
     
    
  • lib/mechanize/file_response.rb+1 1 modified
    @@ -15,7 +15,7 @@ def read_body
         if directory?
           yield dir_body
         else
    -      open @file_path, 'rb' do |io|
    +      ::File.open(@file_path, 'rb') do |io|
             yield io.read
           end
         end
    
  • lib/mechanize.rb+1 1 modified
    @@ -396,7 +396,7 @@ def download uri, io_or_filename, parameters = [], referer = nil, headers = {}
         io = if io_or_filename.respond_to? :write then
                io_or_filename
              else
    -           open io_or_filename, 'wb'
    +           ::File.open(io_or_filename, 'wb')
              end
     
         case page
    
  • lib/mechanize/test_case/gzip_servlet.rb+1 1 modified
    @@ -13,7 +13,7 @@ def do_GET(req, res)
         end
     
         if name = req.query['file'] then
    -      open "#{TEST_DIR}/htdocs/#{name}" do |io|
    +      ::File.open("#{TEST_DIR}/htdocs/#{name}") do |io|
             string = String.new
             zipped = StringIO.new string, 'w'
             Zlib::GzipWriter.wrap zipped do |gz|
    
  • lib/mechanize/test_case.rb+2 2 modified
    @@ -230,9 +230,9 @@ def request(req, *data, &block)
         else
           filename = "htdocs#{path.gsub(/[^\/\\.\w\s]/, '_')}"
           unless PAGE_CACHE[filename]
    -        open("#{Mechanize::TestCase::TEST_DIR}/#{filename}", 'rb') { |io|
    +        ::File.open("#{Mechanize::TestCase::TEST_DIR}/#{filename}", 'rb') do |io|
               PAGE_CACHE[filename] = io.read
    -        }
    +        end
           end
     
           res.body = PAGE_CACHE[filename]
    
  • lib/mechanize/test_case/verb_servlet.rb+4 6 modified
    @@ -1,11 +1,9 @@
     class VerbServlet < WEBrick::HTTPServlet::AbstractServlet
       %w[HEAD GET POST PUT DELETE].each do |verb|
    -    eval <<-METHOD
    -      def do_#{verb}(req, res)
    -        res.header['X-Request-Method'] = #{verb.dump}
    -        res.body = #{verb.dump}
    -      end
    -    METHOD
    +    define_method "do_#{verb}" do |req, res|
    +      res.header['X-Request-Method'] = verb
    +      res.body = verb
    +    end
       end
     end
     
    
  • test/test_mechanize_cookie_jar.rb+30 0 modified
    @@ -1,4 +1,5 @@
     require 'mechanize/test_case'
    +require 'fileutils'
     
     class TestMechanizeCookieJar < Mechanize::TestCase
     
    @@ -500,6 +501,35 @@ def test_save_and_read_cookiestxt_with_session_cookies
         assert_equal(0, @jar.cookies(url).length)
       end
     
    +  def test_prevent_command_injection_when_saving
    +    url = URI 'http://rubygems.org/'
    +    path = '| ruby -rfileutils -e \'FileUtils.touch("vul.txt")\''
    +
    +    @jar.add(url, Mechanize::Cookie.new(cookie_values))
    +
    +    in_tmpdir do
    +      @jar.save_as(path, :cookiestxt)
    +      assert_equal(false, File.exist?('vul.txt'))
    +    end
    +  end
    +
    +  def test_prevent_command_injection_when_loading
    +    url = URI 'http://rubygems.org/'
    +    path = '| ruby -rfileutils -e \'FileUtils.touch("vul.txt")\''
    +
    +    @jar.add(url, Mechanize::Cookie.new(cookie_values))
    +
    +    in_tmpdir do
    +      @jar.save_as("cookies.txt", :cookiestxt)
    +      @jar.clear!
    +
    +      assert_raises Errno::ENOENT do
    +        @jar.load(path, :cookiestxt)
    +      end
    +      assert_equal(false, File.exist?('vul.txt'))
    +    end
    +  end
    +
       def test_save_and_read_expired_cookies
         url = URI 'http://rubygems.org/'
     
    
  • test/test_mechanize_download.rb+12 1 modified
    @@ -46,6 +46,18 @@ def test_save_bang
         end
       end
     
    +  def test_save_bang_does_not_allow_command_injection
    +    uri = URI.parse 'http://example/foo.html'
    +    body_io = StringIO.new '0123456789'
    +
    +    download = @parser.new uri, nil, body_io
    +
    +    in_tmpdir do
    +      download.save!('| ruby -rfileutils -e \'FileUtils.touch("vul.txt")\'')
    +      refute_operator(File, :exist?, "vul.txt")
    +    end
    +  end
    +
       def test_save_tempfile
         uri = URI.parse 'http://example/foo.html'
         Tempfile.open @NAME do |body_io|
    @@ -84,6 +96,5 @@ def test_filename
     
         assert_equal "foo.html", download.filename
       end
    -
     end
     
    
  • test/test_mechanize_file.rb+9 0 modified
    @@ -103,5 +103,14 @@ def test_save_overwrite
         end
       end
     
    +  def test_save_bang_does_not_allow_command_injection
    +    uri = URI 'http://example/test.html'
    +    page = Mechanize::File.new uri, nil, ''
    +
    +    in_tmpdir do
    +      page.save!('| ruby -rfileutils -e \'FileUtils.touch("vul.txt")\'')
    +      refute_operator(File, :exist?, "vul.txt")
    +    end
    +  end
     end
     
    
  • test/test_mechanize_file_response.rb+20 2 modified
    @@ -1,7 +1,6 @@
     require 'mechanize/test_case'
     
     class TestMechanizeFileResponse < Mechanize::TestCase
    -
       def test_content_type
         Tempfile.open %w[pi .nothtml] do |tempfile|
           res = Mechanize::FileResponse.new tempfile.path
    @@ -19,5 +18,24 @@ def test_content_type
         end
       end
     
    -end
    +  def test_read_body
    +    Tempfile.open %w[pi .html] do |tempfile|
    +      tempfile.write("asdfasdfasdf")
    +      tempfile.close
     
    +      res = Mechanize::FileResponse.new(tempfile.path)
    +      res.read_body do |input|
    +        assert_equal("asdfasdfasdf", input)
    +      end
    +    end
    +  end
    +
    +  def test_read_body_does_not_allow_command_injection
    +    in_tmpdir do
    +      FileUtils.touch('| ruby -rfileutils -e \'FileUtils.touch("vul.txt")\'')
    +      res = Mechanize::FileResponse.new('| ruby -rfileutils -e \'FileUtils.touch("vul.txt")\'')
    +      res.read_body { |_| }
    +      refute_operator(File, :exist?, "vul.txt")
    +    end
    +  end
    +end
    
  • test/test_mechanize.rb+8 0 modified
    @@ -345,6 +345,14 @@ def test_download_filename_error
         end
       end
     
    +  def test_download_does_not_allow_command_injection
    +    in_tmpdir do
    +      @mech.download('http://example', '| ruby -rfileutils -e \'FileUtils.touch("vul.txt")\'')
    +
    +      refute_operator(File, :exist?, "vul.txt")
    +    end
    +  end
    +
       def test_get
         uri = URI 'http://localhost'
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

14

News mentions

0

No linked articles in our index yet.