Command Injection Vulnerability in Mechanize
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.
| Package | Affected versions | Patched versions |
|---|---|---|
mechanizeRubyGems | >= 2.0.0, < 2.7.7 | 2.7.7 |
Affected products
2- sparklemotion/mechanizev5Range: >= 2.0, < 2.7.7
Patches
166a6a1bfa653Merge pull request #548 from kyoshidajp/fix_command_injection
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- github.com/advisories/GHSA-qrqm-fpv6-6r8gghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LBVVJUL4P4KCJH4IQTHFZ4ATXY7XXZPV/mitrevendor-advisoryx_refsource_FEDORA
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/YNFZ7ROYS6V4J5L5PRAJUG2AWC7VXR2V/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2021-21289ghsaADVISORY
- security.gentoo.org/glsa/202107-17ghsavendor-advisoryx_refsource_GENTOOWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/mechanize/CVE-2021-21289.ymlghsaWEB
- github.com/sparklemotion/mechanize/commit/66a6a1bfa653a5f13274a396a5e5441238656aa0ghsax_refsource_MISCWEB
- github.com/sparklemotion/mechanize/releases/tag/v2.7.7ghsax_refsource_MISCWEB
- github.com/sparklemotion/mechanize/security/advisories/GHSA-qrqm-fpv6-6r8gghsax_refsource_CONFIRMWEB
- lists.debian.org/debian-lts-announce/2021/02/msg00021.htmlghsamailing-listx_refsource_MLISTWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/LBVVJUL4P4KCJH4IQTHFZ4ATXY7XXZPVghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/YNFZ7ROYS6V4J5L5PRAJUG2AWC7VXR2VghsaWEB
- rubygems.org/gems/mechanizeghsaWEB
- rubygems.org/gems/mechanize/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.