diff --git a/lib/browserstack/local.rb b/lib/browserstack/local.rb index 0a01b1c..5e1417d 100644 --- a/lib/browserstack/local.rb +++ b/lib/browserstack/local.rb @@ -121,12 +121,27 @@ def stop @pid = nil end + # Public accessor used by callers for debugging/logging. Return the command + # with the access key masked so it is never written to logs, CI artifacts or + # error trackers (CWE-312). The real key is still used for execution via + # start_command_args / start_command(false). def command - start_command + start_command(true) end - def start_command - cmd = "#{@binary_path} -d start -logFile '#{@logfile}' #{@folder_flag} #{@key} #{@folder_path} #{@force_local_flag}" + # Prevent Ruby's default #inspect from dumping @key when a Local instance is + # logged or included in an exception payload (CWE-312). + def inspect + redacted = instance_variables.map do |var| + value = var == :@key && !@key.to_s.empty? ? "[REDACTED]" : instance_variable_get(var) + "#{var}=#{value.inspect}" + end.join(", ") + "#<#{self.class}:0x#{format('%016x', object_id << 1)} #{redacted}>" + end + + def start_command(redact = false) + key = redact && !@key.to_s.empty? ? "[REDACTED]" : @key + cmd = "#{@binary_path} -d start -logFile '#{@logfile}' #{@folder_flag} #{key} #{@folder_path} #{@force_local_flag}" cmd += " -localIdentifier #{@local_identifier_flag}" if @local_identifier_flag cmd += " #{@only_flag} #{@only_automate_flag}" cmd += " -proxyHost #{@proxy_host}" if @proxy_host diff --git a/test/browserstack-local-test.rb b/test/browserstack-local-test.rb index 2c6218b..12c96ed 100644 --- a/test/browserstack-local-test.rb +++ b/test/browserstack-local-test.rb @@ -96,6 +96,27 @@ def test_hosts assert_match /localhost\,8080\,0/, @bs_local.command end + # Regression for CWE-312: the public #command accessor must NOT expose the + # access key — callers routinely log it to CI output / APM / error trackers. + def test_command_redacts_access_key + bs = BrowserStack::Local.new("MY_SECRET_ACCESS_KEY") + refute_match /MY_SECRET_ACCESS_KEY/, bs.command + assert_match /\[REDACTED\]/, bs.command + end + + # The real key must still reach the binary on the execution path. + def test_start_command_keeps_key_for_execution + bs = BrowserStack::Local.new("MY_SECRET_ACCESS_KEY") + assert_match /MY_SECRET_ACCESS_KEY/, bs.start_command + end + + # Regression for CWE-312: default object inspection must not dump the key. + def test_inspect_redacts_access_key + bs = BrowserStack::Local.new("MY_SECRET_ACCESS_KEY") + refute_match /MY_SECRET_ACCESS_KEY/, bs.inspect + assert_match /\[REDACTED\]/, bs.inspect + end + def teardown @bs_local.stop end