Skip to content

Commit 66a6a1b

Browse files
authored
Merge pull request #548 from kyoshidajp/fix_command_injection
Prevent OS command injection
2 parents 687c538 + e238b07 commit 66a6a1b

14 files changed

+108
-18
lines changed

CHANGELOG.rdoc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
=== Unreleased
44

5+
* Security
6+
7+
Mechanize `>= v2.0`, `< v2.7.7` allows for OS commands to be injected into several classes'
8+
methods via implicit use of Ruby's `Kernel.open` method. Exploitation is possible only if
9+
untrusted input is used as a local filename and passed to any of these calls:
10+
11+
- `Mechanize::CookieJar#load`: since v2.0 (see 208e3ed)
12+
- `Mechanize::CookieJar#save_as`: since v2.0 (see 5b776a4)
13+
- `Mechanize#download`: since v2.2 (see dc91667)
14+
- `Mechanize::Download#save` and `#save!` since v2.1 (see 98b2f51, bd62ff0)
15+
- `Mechanize::File#save` and `#save_as`: since v2.1 (see 2bf7519)
16+
- `Mechanize::FileResponse#read_body`: since v2.0 (see 01039f5)
17+
18+
See https://github.com/sparklemotion/mechanize/security/advisories/GHSA-qrqm-fpv6-6r8g for more
19+
information.
20+
521
* New Features
622
* Support for Ruby 3.0 by adding `webrick` as a runtime dependency. (#557) @pvalena
723

lib/mechanize.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def download uri, io_or_filename, parameters = [], referer = nil, headers = {}
396396
io = if io_or_filename.respond_to? :write then
397397
io_or_filename
398398
else
399-
open io_or_filename, 'wb'
399+
::File.open(io_or_filename, 'wb')
400400
end
401401

402402
case page

lib/mechanize/cookie_jar.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def dump_cookiestxt(io)
6565
class CookieJar < ::HTTP::CookieJar
6666
def save(output, *options)
6767
output.respond_to?(:write) or
68-
return open(output, 'w') { |io| save(io, *options) }
68+
return ::File.open(output, 'w') { |io| save(io, *options) }
6969

7070
opthash = {
7171
:format => :yaml,
@@ -119,7 +119,7 @@ def save(output, *options)
119119

120120
def load(input, *options)
121121
input.respond_to?(:write) or
122-
return open(input, 'r') { |io| load(io, *options) }
122+
return ::File.open(input, 'r') { |io| load(io, *options) }
123123

124124
opthash = {
125125
:format => :yaml,

lib/mechanize/download.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def save! filename = nil
7171
dirname = File.dirname filename
7272
FileUtils.mkdir_p dirname
7373

74-
open filename, 'wb' do |io|
74+
::File.open(filename, 'wb')do |io|
7575
until @body_io.eof? do
7676
io.write @body_io.read 16384
7777
end

lib/mechanize/file.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def save! filename = nil
8282
dirname = File.dirname filename
8383
FileUtils.mkdir_p dirname
8484

85-
open filename, 'wb' do |f|
85+
::File.open(filename, 'wb')do |f|
8686
f.write body
8787
end
8888

lib/mechanize/file_response.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def read_body
1515
if directory?
1616
yield dir_body
1717
else
18-
open @file_path, 'rb' do |io|
18+
::File.open(@file_path, 'rb') do |io|
1919
yield io.read
2020
end
2121
end

lib/mechanize/test_case.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,9 @@ def request(req, *data, &block)
230230
else
231231
filename = "htdocs#{path.gsub(/[^\/\\.\w\s]/, '_')}"
232232
unless PAGE_CACHE[filename]
233-
open("#{Mechanize::TestCase::TEST_DIR}/#{filename}", 'rb') { |io|
233+
::File.open("#{Mechanize::TestCase::TEST_DIR}/#{filename}", 'rb') do |io|
234234
PAGE_CACHE[filename] = io.read
235-
}
235+
end
236236
end
237237

238238
res.body = PAGE_CACHE[filename]

lib/mechanize/test_case/gzip_servlet.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def do_GET(req, res)
1313
end
1414

1515
if name = req.query['file'] then
16-
open "#{TEST_DIR}/htdocs/#{name}" do |io|
16+
::File.open("#{TEST_DIR}/htdocs/#{name}") do |io|
1717
string = String.new
1818
zipped = StringIO.new string, 'w'
1919
Zlib::GzipWriter.wrap zipped do |gz|
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
class VerbServlet < WEBrick::HTTPServlet::AbstractServlet
22
%w[HEAD GET POST PUT DELETE].each do |verb|
3-
eval <<-METHOD
4-
def do_#{verb}(req, res)
5-
res.header['X-Request-Method'] = #{verb.dump}
6-
res.body = #{verb.dump}
7-
end
8-
METHOD
3+
define_method "do_#{verb}" do |req, res|
4+
res.header['X-Request-Method'] = verb
5+
res.body = verb
6+
end
97
end
108
end
119

test/test_mechanize.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,14 @@ def test_download_filename_error
345345
end
346346
end
347347

348+
def test_download_does_not_allow_command_injection
349+
in_tmpdir do
350+
@mech.download('http://example', '| ruby -rfileutils -e \'FileUtils.touch("vul.txt")\'')
351+
352+
refute_operator(File, :exist?, "vul.txt")
353+
end
354+
end
355+
348356
def test_get
349357
uri = URI 'http://localhost'
350358

0 commit comments

Comments
 (0)