Skip to content

Commit 9529fed

Browse files
07souravkundaclaude
andcommitted
fix: redact access key in public command accessor and inspect (CWE-312)
The public `command` method returned the start command string with the BrowserStack access key interpolated verbatim, so any caller that logged it (CI output, test runner logs, APM/error trackers) leaked the credential to a wider audience than the key itself. Ruby's default #inspect had the same problem, dumping @key when a Local instance was logged or raised. - command now returns the command with the key masked as [REDACTED] - start_command takes an optional redact flag; the execution path (start_command_args array, and the string form on legacy Ruby) keeps the real key, so the tunnel is unaffected - add a redacting #inspect so the key is never dumped by object inspection Proxy password is intentionally left visible (existing behaviour/tests). Adds regression tests that fail on the pre-fix code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6a67875 commit 9529fed

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

‎lib/browserstack/local.rb‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,27 @@ def stop
121121
@pid = nil
122122
end
123123

124+
# Public accessor used by callers for debugging/logging. Return the command
125+
# with the access key masked so it is never written to logs, CI artifacts or
126+
# error trackers (CWE-312). The real key is still used for execution via
127+
# start_command_args / start_command(false).
124128
def command
125-
start_command
129+
start_command(true)
126130
end
127131

128-
def start_command
129-
cmd = "#{@binary_path} -d start -logFile '#{@logfile}' #{@folder_flag} #{@key} #{@folder_path} #{@force_local_flag}"
132+
# Prevent Ruby's default #inspect from dumping @key when a Local instance is
133+
# logged or included in an exception payload (CWE-312).
134+
def inspect
135+
redacted = instance_variables.map do |var|
136+
value = var == :@key && !@key.to_s.empty? ? "[REDACTED]" : instance_variable_get(var)
137+
"#{var}=#{value.inspect}"
138+
end.join(", ")
139+
"#<#{self.class}:0x#{format('%016x', object_id << 1)} #{redacted}>"
140+
end
141+
142+
def start_command(redact = false)
143+
key = redact && !@key.to_s.empty? ? "[REDACTED]" : @key
144+
cmd = "#{@binary_path} -d start -logFile '#{@logfile}' #{@folder_flag} #{key} #{@folder_path} #{@force_local_flag}"
130145
cmd += " -localIdentifier #{@local_identifier_flag}" if @local_identifier_flag
131146
cmd += " #{@only_flag} #{@only_automate_flag}"
132147
cmd += " -proxyHost #{@proxy_host}" if @proxy_host

‎test/browserstack-local-test.rb‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ def test_hosts
9696
assert_match /localhost\,8080\,0/, @bs_local.command
9797
end
9898

99+
# Regression for CWE-312: the public #command accessor must NOT expose the
100+
# access key — callers routinely log it to CI output / APM / error trackers.
101+
def test_command_redacts_access_key
102+
bs = BrowserStack::Local.new("MY_SECRET_ACCESS_KEY")
103+
refute_match /MY_SECRET_ACCESS_KEY/, bs.command
104+
assert_match /\[REDACTED\]/, bs.command
105+
end
106+
107+
# The real key must still reach the binary on the execution path.
108+
def test_start_command_keeps_key_for_execution
109+
bs = BrowserStack::Local.new("MY_SECRET_ACCESS_KEY")
110+
assert_match /MY_SECRET_ACCESS_KEY/, bs.start_command
111+
end
112+
113+
# Regression for CWE-312: default object inspection must not dump the key.
114+
def test_inspect_redacts_access_key
115+
bs = BrowserStack::Local.new("MY_SECRET_ACCESS_KEY")
116+
refute_match /MY_SECRET_ACCESS_KEY/, bs.inspect
117+
assert_match /\[REDACTED\]/, bs.inspect
118+
end
119+
99120
def teardown
100121
@bs_local.stop
101122
end

0 commit comments

Comments
 (0)