Repository navigation
Mask password fields in the replayed page by default - #530
Open
vishnuv688 wants to merge 5 commits into
Open
vishnuv688 wants to merge 5 commits into
vishnuv688 wants to merge 5 commits into
Conversation
A trace recorded everything the run typed, credentials included, several times over: the DOM mutation stream (keystroke by keystroke), the field state, input commands' arguments, the transcript, values read back and assertion errors. With a hosted player inviting traces from other people, that had to stop being the default. Typed text is now replaced by a fixed placeholder that hides its length before it leaves the browser or the adapter process: page-side in the collector for text-like inputs, textareas and editable regions, and adapter-side for the typed arguments of input commands in each vocabulary. Each typed value is remembered and scrubbed wherever it comes back: rows, errors (test and hook errors stored as scrubbed copies, never mutating the runner's own), console and log lines, and request and response bodies. Only the recorded copy is masked; the driver still receives what the test typed. maskSelectors masks more, maskInputs: false restores full capture, and the README lists what is not covered (pixels, test source, text the page displays by itself). All four adapters, with Python mirrored and checked by shared mirror vectors and a conformance rule. Closes #503 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Masking every typed value hid the username in the replay and showed
setValue("#username", "••••") in the Log, which made rows hard to read.
Command rows and the Log now always carry the real typed value. By
default the page masks the value of password fields only (kept masked
after a show-password toggle); maskSelectors masks anything else, its
text and its aria-label, title, alt and placeholder, and never lets that
text reach an ancestor's name or locator. maskInputs: false turns the
password masking off.
Argument masking, key-action collapsing and the scrubbing of typed
values from errors, console and bodies are removed in every adapter.
The conformance rule now checks that each adapter hands the page its
mask policy and keeps typed text on its rows. The pytest plugin treats
an unset ini answered as [] as unset, and an empty DEVTOOLS_MASK_INPUTS
keeps masking on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/selenium-devtools-py/tests/test_mirror_vectors.py # packages/selenium-devtools/tests/mirror-vectors.test.ts
The mask policy is written into the collector's script source as JSON, and a user's maskSelectors value is part of it. A selector holding </script>, */ or a line separator could end the script or a comment around it. The prelude now writes <, >, /, U+2028 and U+2029 as \uXXXX escapes, which JSON reads back as the same text; Python writes the same bytes, checked by a new mirror case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # README.md # packages/conformance/src/features.ts
Member
Author
|
Docs site: webdriverio/webdriverio#16378 (draft, merges after this). |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #503. Part of #494.
The trace replay records every field as it was typed, so a password typed into a login form showed in the replayed page, keystroke by keystroke. Now that traces can be opened on a hosted player and shared, the replay should not show passwords by default.
The replayed page now masks password fields by default, on all four adapters, in live and trace mode. Their value is shown as
••••(a fixed placeholder that hides the length) in the DOM snapshot, the field state and every value change. Masking happens in the browser, before the collector hands anything to the adapter.Command rows and the Log section always show the real typed value, passwords included:
setValue("#username", "tomsmith")reads as written. Only the replayed page is masked.Options:
maskInputs(defaulttrue): mask<input type="password">values in the replayed page. A field that was a password field when the collector saw it stays masked after a show-password toggle turns it into a text field.maskInputs: falseturns this off.maskSelectors: also mask the text and value of matching elements and everything inside them, plus theiraria-label,title,altandplaceholder. Their text never reaches an ancestor's accessible name or text locator.enable(mask_inputs=..., mask_selectors=[...]),--devtools-no-mask-inputs,--devtools-mask-selector, ini keysdevtools_mask_inputs/devtools_mask_selectors, envDEVTOOLS_MASK_INPUTS/DEVTOOLS_MASK_SELECTORS.Type of change
Packages touched
shared(types and contracts)core(framework-agnostic capture/reporting)elements(published element/snapshot API —@wdio/elements)service(WebdriverIO adapter)nightwatch-devtools(Nightwatch adapter)selenium-devtools(Selenium adapter)selenium-devtools-py(Selenium Python adapter)backend(server)app(UI)script(page-injected runtime)trace(Trace mode)Plus
conformance(a rule and the fake driver).elementsandbackendbundle the changed shared/script code (patch changesets) but have no source changes.Capability matrix
packages/conformance/src/features.tsupdated, and tables regenerated withpnpm docs:capabilities, if this adds or changes user-facing behaviourFeature
input-masking, backed for all four adapters by the conformance rulepassword-masked-only-in-the-page; options declared incapability-options.ts. The docs-site tables are refreshed with the docs-site PR.Notes for reviewers
Why mostly
scriptandshared: the masking runs in the page collector (script/src/masking.ts) and the element scripts (shared/src/element-scripts.ts). The policy reaches the page as a short prelude ahead of the collector source (shared/src/input-mask.ts,core/src/capture-mask.ts), on both the BiDi preload and the<script>fallback. Each adapter only resolves its options into that policy. Python mirrors the prelude, checked againstmirror-vectors.json(collectorMaskPrelude).History of this PR: the first commit masked every typed value everywhere: page, command arguments, and scrubbing from errors, console and bodies. Seeing
setValue("#username", "••••")in the Log made rows unreadable, so the second commit narrows masking to password fields in the replayed page and removes the argument masking and scrubbing entirely. Read the PR as one diff against main.Evidence: the conformance rule checks, for every adapter, that each collector source starts with the mask policy and that the typed text stays on its rows. The page masking itself is covered by
packages/script/tests/masking.test.ts(password-only default, show-password toggle,maskSelectorswith attributes,maskInputs: false). The element-scripts tests check that masked text reaches no ancestor's name or locator.Not covered (also in the README "Password masking" section):
maskSelectors);value,aria-label,title,altandplaceholderunder a mask selector;Review: an independent review of the rework found two issues to fix before merge: Python reading an unset pytest 7 ini as "off", and masked text reaching an ancestor's name. Both are fixed, along with the smaller items.
Testing
pnpm build,pnpm test(3573 passed, 1 skipped),pnpm lint,pnpm docs:capabilities --checkare green; no workspace import leaks in any bundle.test_instrumentation.pytests fail locally on Python 3.12 both with and without this PR; CI is the check there.Needs checking by hand (optionally
pnpm smoke:fixturesandDEVTOOLS_EXAMPLE_BASE_URL=http://127.0.0.1:4567)pnpm demo:wdio:mocha,pnpm demo:nightwatch,pnpm demo:selenium,pnpm demo:python:pytest: clicking the username row showstomsmithin Log and in the replayed field; clicking the password row shows the real password in Log and••••in the replayed password field.DEVTOOLS_MODE=trace), opened withpnpm show-trace <zip>: the same as above.maskSelectors: ['.subheader']on one adapter: the subheader text shows••••in the replay.maskInputs: falseon one adapter: the password field shows its value in the replay.Not in this PR: the docs-site mirror of the README section and capability tables.
Screenshots / recordings
To be added: the username and password rows with Log and the replayed field.
🤖 Generated with Claude Code