|
1 | 1 | require 'rubygems' |
2 | 2 | require 'minitest' |
3 | 3 | require 'minitest/autorun' |
| 4 | +require 'minitest/mock' |
| 5 | +require 'tmpdir' |
4 | 6 | require 'browserstack/local' |
5 | 7 |
|
6 | 8 | class BrowserStackLocalTest < Minitest::Test |
@@ -112,6 +114,61 @@ def teardown |
112 | 114 | end |
113 | 115 | end |
114 | 116 |
|
| 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 | + |
115 | 172 | class BrowserStackLocalBinaryTest < Minitest::Test |
116 | 173 | def test_default_user_agent_contains_gem_name_and_version |
117 | 174 | ua = BrowserStack::LocalBinary.new(auth_token: 'fake').instance_variable_get(:@user_agent) |
|
0 commit comments