Skip to content

Commit 614d949

Browse files
Merge pull request #45 from browserstack/locsec/WI-09e205d7
fix: create the logfile without a shell (CWE-78 command injection)
2 parents 861f33a + b7b0d1a commit 614d949

2 files changed

Lines changed: 68 additions & 4 deletions

File tree

‎lib/browserstack/local.rb‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'browserstack/localbinary'
22
require 'browserstack/localexception'
33
require 'json'
4+
require 'fileutils'
45

56
module BrowserStack
67

@@ -73,10 +74,16 @@ def start(options = {})
7374
@binary_path
7475
end
7576

76-
if @is_windows
77-
system("echo > #{@logfile}")
78-
else
79-
system("echo '' > '#{@logfile}'")
77+
# Create/truncate the logfile without a shell. The previous
78+
# `system("echo ... > #{@logfile}")` passed @logfile to /bin/sh (or cmd.exe),
79+
# so shell metacharacters in a caller-supplied logfile path executed as commands
80+
# (CWE-78). File.write treats the path purely as a filename.
81+
logfile_dir = File.dirname(@logfile)
82+
FileUtils.mkdir_p(logfile_dir) unless File.directory?(logfile_dir)
83+
begin
84+
File.write(@logfile, "")
85+
rescue SystemCallError => e
86+
raise BrowserStack::LocalException.new("Unable to open logfile: #{e.message}")
8087
end
8188

8289
if defined? spawn

‎test/browserstack-local-test.rb‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
require 'rubygems'
22
require 'minitest'
33
require 'minitest/autorun'
4+
require 'minitest/mock'
5+
require 'tmpdir'
46
require 'browserstack/local'
57

68
class BrowserStackLocalTest < Minitest::Test
@@ -112,6 +114,61 @@ def teardown
112114
end
113115
end
114116

117+
# Regression tests for the logfile-creation step in Local#start (CWE-78).
118+
# The logfile used to be created with `system("echo ... > #{@logfile}")`, which
119+
# passed the caller-supplied path through a shell. These tests drive the public
120+
# `start` entry point but abort just after the logfile step (a fake binarypath
121+
# skips the download; stubbing start_command_args prevents launching the binary),
122+
# so they need no credentials, network, or tunnel.
123+
class BrowserStackLocalLogfileTest < Minitest::Test
124+
class AbortAfterLogfile < StandardError; end
125+
126+
# Runs `start` with the given logfile value, aborting right after the logfile
127+
# is created (before the real binary is spawned).
128+
def start_up_to_logfile(logfile_value)
129+
bs = BrowserStack::Local.new('dummy_key')
130+
bs.stub(:start_command_args, ->(*) { raise AbortAfterLogfile }) do
131+
begin
132+
# An existing, harmless executable as binarypath skips the binary download.
133+
bs.start('binarypath' => existing_executable, 'logfile' => logfile_value)
134+
rescue AbortAfterLogfile
135+
# expected: we intentionally stop before launching the binary
136+
end
137+
end
138+
end
139+
140+
def existing_executable
141+
['/bin/true', '/usr/bin/true'].find { |p| File.executable?(p) } || RbConfig.ruby
142+
end
143+
144+
def test_shell_metacharacters_in_logfile_path_are_not_executed
145+
Dir.mktmpdir do |dir|
146+
Dir.chdir(dir) do
147+
marker = File.join(dir, 'pwned')
148+
# Unix payload: close the single quote around @logfile, run touch, reopen.
149+
# Pre-fix this expands to: echo '' > 'log' ; touch <marker> ; echo 'x'
150+
payload = "log' ; touch #{marker} ; echo 'x"
151+
152+
start_up_to_logfile(payload)
153+
154+
refute File.exist?(marker),
155+
'shell metacharacters in the logfile path were executed (command injection)'
156+
end
157+
end
158+
end
159+
160+
def test_logfile_path_is_treated_as_a_literal_filename
161+
Dir.mktmpdir do |dir|
162+
logfile = File.join(dir, 'sub', 'my log.txt') # spaces + missing subdir
163+
start_up_to_logfile(logfile)
164+
165+
assert File.file?(logfile),
166+
'the logfile should be created as a literal path, even with spaces / a missing dir'
167+
assert_equal '', File.read(logfile), 'the logfile should be truncated to empty'
168+
end
169+
end
170+
end
171+
115172
class BrowserStackLocalBinaryTest < Minitest::Test
116173
def test_default_user_agent_contains_gem_name_and_version
117174
ua = BrowserStack::LocalBinary.new(auth_token: 'fake').instance_variable_get(:@user_agent)

0 commit comments

Comments
 (0)