Skip to content

fix(SDK-7250): make the config debug dump safe, lossless and opt-out-able - #130

Open
AakashHotchandani wants to merge 1 commit into
mainfrom
feat/sdk-7250-alt-log-route
Open

fix(SDK-7250): make the config debug dump safe, lossless and opt-out-able#130
AakashHotchandani wants to merge 1 commit into
mainfrom
feat/sdk-7250-alt-log-route

Conversation

@AakashHotchandani

Copy link
Copy Markdown
Collaborator

What is this about?

I set out to build the alternative to #128 that came up in review: instead of capturing the user's wdio.conf file, make the config dump the service already writes into bstack-wdio-service.log lossless — stringify the hook functions — and drop the file-capture path entirely. That would have deleted the whole config-resolution ladder.

That specific goal turns out to be impossible. The hardening I built along the way is worth having regardless, so this PR is that hardening, and the finding is written up below so nobody re-attempts it.

Why the log can never replace reading the config file

@wdio/config's ConfigParser.addService binds every hook, including hooks defined in the user's own config file:

this._config[hookName] = hook.bind(service)

Per ECMAScript, a bound function has no source text. Function.prototype.toString() on one returns:

function () { [native code] }

Verified on a real run — every hook in the dump reads ["function () { [native code] }"], and a marker string placed inside a before() hook never appears in the log. No serializer can get it back; the information is gone before we ever see the object.

This is worth knowing because a unit test does not catch it: a plain function () {…} in a test fixture stringifies perfectly. It only shows up against a real merged config. So #128's file capture stands as the only way to see hook bodies.

Details

What this PR does keep:

1. serializeConfigForLog() replaces JSON.parse(JSON.stringify(config)).

before after
RegExp value {} "/\\.e2e\\.ts$/"
circular config throws "[Circular]"
unserializable value throws "[unserializable: …]"

The throw matters more than it looks: that call sits in the service constructor with no enclosing try/catch, so a circular reference from a plugin or reporter takes the constructor down.

2. Credential scrubbing now covers compound key names. The previous list was exact-name only:

CrashReporter.recursivelyRedactKeysFromObject(configCopy, ['user', 'username', 'key', 'accesskey', 'password'])

so clientSecret, client_secret, CLIENT_SECRET and AWS_SECRET_ACCESS_KEY were all written verbatim into a log that gets uploaded. Basic-auth URLs (https://user:pass@host) and inline PEM blocks in string values are scrubbed too. Lookalikes (hotkey, keyword, my_secretary, https://example.com:8080/path) are deliberately left alone.

3. _options data: and webdriver capabilities data: had no object-level redaction at all — raw JSON.stringify. Both now go through the same serializer.

4. Adds disableAutoCaptureLogs (plus BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS), honoured inside uploadLogs itself rather than only at the call site — the detached cleanup rescue in cleanup.ts calls it with no options object, and opting out is precisely what leaves logsUploaded false and arms that rescue.

Relationship to #128

Not a replacement — the two are independent and complementary:

They can land in either order. If both land, the redaction helpers should be de-duplicated into one module — happy to do that as a follow-up on whichever merges second.

How Has This Been Tested?

17 new unit tests; suite is 48 files / 1091 tests green, lint and typecheck clean.

Dev-tested on a real BrowserStack session with a config carrying every case — hooks containing secrets, a one-line hook, a RegExp value, compound/SCREAMING secret keys, a basic-auth URL and a PEM block. Build 3k8ayl4lhhhv2qvoqgxcwqcczku6o6griggkykuq, log upload succeeded.

Scanning the entire uploaded service log:

clean  apiKey inside hook          clean  clientSecret        clean  basic-auth URL
clean  GITHUB_TOKEN inside hook    clean  client_secret       clean  PEM body
clean  real access key             clean  CLIENT_SECRET       clean  real username
clean  AWS_SECRET_ACCESS_KEY

and RegExp survived as "/\\.e2e\\.js$/", with accessibility and lookalike keys still readable.

One honest caveat on that table: the two "inside hook" rows are clean because the hook source never reached the log at all (bound functions), not because scrubbing caught them. If hook bodies were ever recoverable, that scrubbing path is tested by unit test but has no live coverage.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactor (code change that does not change external functionality)

Checklist:

  • I've resolved all linter issues
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.

…able

Investigated replacing the wdio.conf file capture (#128) with a richer log dump, by
stringifying hook functions into `_config data:`. That specific goal is NOT achievable --
see below -- but the surrounding hardening is worth having on its own.

What this does:

- serializeConfigForLog() replaces `JSON.parse(JSON.stringify(config))`. It keeps RegExp
  values instead of collapsing them to `{}`, returns '[Circular]' instead of throwing on a
  circular config, and never throws at all -- the previous call sits in the service
  constructor with no try/catch, so a circular reference from a plugin or reporter took the
  constructor down.
- Credential scrubbing now covers compound key names. The previous exact-name list
  (`user`, `username`, `key`, `accesskey`, `password`) could not see `clientSecret`,
  `client_secret`, `CLIENT_SECRET` or `AWS_SECRET_ACCESS_KEY`, all of which were being
  written to a log that is uploaded. Basic-auth URLs and inline PEM blocks in string values
  are scrubbed too.
- `_options data:` and `webdriver capabilities data:` went through raw JSON.stringify with no
  object-level redaction at all; both now use the same serializer.
- Adds `disableAutoCaptureLogs` (and BROWSERSTACK_DISABLE_AUTO_CAPTURE_LOGS), honoured in
  uploadLogs itself so the detached cleanup rescue is covered, since opting out is exactly
  what leaves logsUploaded false and arms that rescue.

Why the original goal is impossible: @wdio/config ConfigParser.addService does
`hook.bind(service)` on every hook, including hooks defined in the user's own config file.
Per ECMAScript, a bound function has no source text -- `toString()` returns
`function () { [native code] }`. Verified on a real run: every hook in the dump reads
`["function () { [native code] }"]`. No serializer can recover hook bodies from the config
object, so the log cannot substitute for reading the config file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant