Skip to content

fix(security): stop credential/PII leaks at capture, persistence and replay - #169

Open
Sangaibisi wants to merge 2 commits into
browser-use:mainfrom
Sangaibisi:fix/mask-sensitive-recorded-values
Open

fix(security): stop credential/PII leaks at capture, persistence and replay#169
Sangaibisi wants to merge 2 commits into
browser-use:mainfrom
Sangaibisi:fix/mask-sensitive-recorded-values

Conversation

@Sangaibisi

@Sangaibisi Sangaibisi commented Aug 11, 2026

Copy link
Copy Markdown

Problem

Recorded credentials and PII currently leak on three independent paths:

  1. semanticInfo.value carries the RAW value of every field — including passwords. The step's own value is masked to ********, but the semanticInfo object embedded in the same event still contains the cleartext, which is stored in the session log, dumped to the page console (console.log("Sending CUSTOM_INPUT_EVENT:", inputData)), and shipped to the local recorder server.
  2. Masking only covers input[type=password]. OTP fields (type=text/tel + autocomplete=one-time-code), credit-card/CVV fields (type=text/number), SSN/IBAN and password-named fields are recorded in cleartext. We verified this with a real recording where a phone number typed into a tel field landed verbatim in the saved .workflow.yaml.
  3. High-confidence sensitive matches are persisted as plaintext defaults. variable_identifier deliberately sets suggested_default=None for SSN/credit-card/password matches (confidence ≥ 0.95), but _generate_input_schema overrides that intent — entry['default'] = candidate.value — so the recorded secret is written into the saved workflow file anyway, tagged format: ssn / credit-card.

On replay, input values (including ********-masked and unmasked ones) are logged at INFO and embedded in error reports verbatim on both executor paths.

Fix

Extension (capture):

  • new isSensitiveField(): checks type, autocomplete (one-time-code, cc-number, cc-csc, cc-exp, new-password, current-password) and name/id/aria-label/placeholder conventions
  • extractSemanticInfo masks the value of sensitive fields — the raw value never enters the event stream
  • handleInput masks by isSensitiveField instead of type === 'password' only
  • per-event console dumps reduced to event-type-only lines (no payload objects)

Python (persistence + replay):

  • _generate_input_schema respects suggested_default=None: sensitive defaults are omitted instead of falling back to the raw recorded value
  • new workflow_use/workflow/redaction.py: redact_step_value() masks values whose field hints look sensitive; wired into the semantic executor's input log, ErrorContext.input_value, and the deterministic controller's input log

No behavior change for non-sensitive fields.

Related: #165 (audit findings), #166, #168.

🤖 Generated with Claude Code


Summary by cubic

Prevents credential and PII leaks by masking sensitive values at capture, persistence, and replay. rrweb input events now use maskAllInputs, and logs/error reports no longer expose raw secrets; non-sensitive inputs behave the same.

  • Bug Fixes
    • Capture (extension): rrweb maskAllInputs; broaden sensitive field detection (type, autocomplete, and name/label hints incl. tel/phone, EN/TR); mask semanticInfo.value and input values; console logs omit payloads.
    • Persistence: _generate_input_schema omits defaults for sensitive types and for sensitive name/context hints; no '********' defaults; these inputs are forced required.
    • Replay: redact_step_value redacts inputs in INFO logs and error reports using hints incl. elementText, elementTag, inputType; applied in the semantic executor, deterministic controller, and error context.

Written for commit c075b6d. Summary will update on new commits.

Review in cubic

…replay

Secrets were mishandled at every stage; this closes the P0 set:

Capture (extension):
- semanticInfo.value carried the RAW value of every field - real
  passwords leaked into stored events and to the recorder server even
  while the step's own value was masked. Sensitive values are masked
  inside extractSemanticInfo now.
- masking covered only input[type=password]; OTP (autocomplete=
  one-time-code), credit-card/CVV, SSN/TCKN/IBAN and password-named
  text/tel/number fields were recorded in cleartext. New
  isSensitiveField() checks type, autocomplete and name/id/label/
  placeholder conventions (EN+TR).
- per-event console.log payload dumps (input/click/select/key data
  objects, typed values included) reduced to event-type-only lines.

Persistence (variable identifier):
- high-confidence sensitive matches (SSN, credit card - confidence
  >= 0.95) deliberately set suggested_default=None, but
  _generate_input_schema overrode that intent and always persisted the
  raw recorded value as a plaintext 'default' in the saved
  .workflow.yaml. Sensitive defaults are now omitted end-to-end.

Replay (engine):
- input values were logged at INFO and embedded in error reports
  verbatim on both executor paths. New workflow_use/workflow/redaction
  module masks values whose field hints look sensitive; wired into the
  semantic executor's input log, ErrorContext.input_value and the
  deterministic controller's input action.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="workflows/workflow_use/controller/service.py">

<violation number="1" location="workflows/workflow_use/controller/service.py:142">
P1: Sensitive replay inputs can still be logged verbatim when the selector is generic or uses standard credit-card autocomplete hints, because redaction receives only the action params and its heuristic misses those cases. Passing the matched element's type/autocomplete/name metadata into redaction (and covering the standard hints) would preserve masking for these fields.</violation>
</file>

<file name="workflows/workflow_use/workflow/semantic_executor.py">

<violation number="1" location="workflows/workflow_use/workflow/semantic_executor.py:1535">
P1: Replay of a recorded telephone or `cc-*` credit-card input can still write its raw value to INFO logs and error reports. This call only uses a hint regex that does not inspect the recorded input type/semantic metadata or recognize `tel` and `cc-number`/`cc-csc`/`cc-exp`, so the shared redactor should be extended before relying on it here.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread extension/src/entrypoints/content.ts
await asyncio.sleep(0.5)

msg = f'⌨️ Input "{params.value}" into element with CSS selector: {truncate_selector(selector_used)} (original: {truncate_selector(original_selector)})'
logged_value = redact_step_value(params, params.value)

@cubic-dev-ai cubic-dev-ai Bot Aug 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Sensitive replay inputs can still be logged verbatim when the selector is generic or uses standard credit-card autocomplete hints, because redaction receives only the action params and its heuristic misses those cases. Passing the matched element's type/autocomplete/name metadata into redaction (and covering the standard hints) would preserve masking for these fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At workflows/workflow_use/controller/service.py, line 142:

<comment>Sensitive replay inputs can still be logged verbatim when the selector is generic or uses standard credit-card autocomplete hints, because redaction receives only the action params and its heuristic misses those cases. Passing the matched element's type/autocomplete/name metadata into redaction (and covering the standard hints) would preserve masking for these fields.</comment>

<file context>
@@ -138,7 +139,8 @@ async def input(
 				await asyncio.sleep(0.5)
 
-				msg = f'⌨️  Input "{params.value}" into element with CSS selector: {truncate_selector(selector_used)} (original: {truncate_selector(original_selector)})'
+				logged_value = redact_step_value(params, params.value)
+				msg = f'⌨️  Input "{logged_value}" into element with CSS selector: {truncate_selector(selector_used)} (original: {truncate_selector(original_selector)})'
 				logger.info(msg)
</file context>
Fix with cubic

await asyncio.sleep(0.5)

msg = f"⌨️ Input '{step.value}' into: {target_identifier or step.description or selector_to_use}"
msg = f"⌨️ Input '{redact_step_value(step, step.value)}' into: {target_identifier or step.description or selector_to_use}"

@cubic-dev-ai cubic-dev-ai Bot Aug 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Replay of a recorded telephone or cc-* credit-card input can still write its raw value to INFO logs and error reports. This call only uses a hint regex that does not inspect the recorded input type/semantic metadata or recognize tel and cc-number/cc-csc/cc-exp, so the shared redactor should be extended before relying on it here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At workflows/workflow_use/workflow/semantic_executor.py, line 1535:

<comment>Replay of a recorded telephone or `cc-*` credit-card input can still write its raw value to INFO logs and error reports. This call only uses a hint regex that does not inspect the recorded input type/semantic metadata or recognize `tel` and `cc-number`/`cc-csc`/`cc-exp`, so the shared redactor should be extended before relying on it here.</comment>

<file context>
@@ -1531,7 +1532,7 @@ async def input_executor():
 			await asyncio.sleep(0.5)
 
-			msg = f"⌨️ Input '{step.value}' into: {target_identifier or step.description or selector_to_use}"
+			msg = f"⌨️ Input '{redact_step_value(step, step.value)}' into: {target_identifier or step.description or selector_to_use}"
 			logger.info(msg)
 			return ActionResult(extracted_content=msg, include_in_memory=True)
</file context>
Fix with cubic

Comment thread workflows/workflow_use/workflow/redaction.py Outdated
Comment thread workflows/workflow_use/workflow/variable_identifier.py Outdated
Comment thread extension/src/entrypoints/content.ts
Sangaibisi added a commit to Sangaibisi/workflow-use that referenced this pull request Aug 11, 2026
…se#169 (16 issues)

Replay engine (PR browser-use#168 feedback):
- text fallback (_find_by_tag_and_text) rewritten as one page-side pass
  in the compat layer: scans ALL candidates (no 40-element cap), matches
  rendered innerText instead of hidden textContent plus aria-label/
  title/placeholder/value, and POLLS until timeout_ms (the old version
  ignored its timeout and queried once)
- XPath/text marker attributes are now per-lookup tokens
  (secrets.token_hex) so overlapping lookups can't fetch each other's
  element; marked-element fetch/cleanup extracted to one helper
- XPath click path decoded: page.evaluate returns a JSON string, and
  .get() on it raised AFTER the click already fired in the page
  (click-then-report-failure trap)
- element_finder._node_attr also reads browser-use's nested
  {'attributes': {...}} dict shape for dict-form nodes
- removed unused compat element_inner_text;
  semantic_executor._set_checked_by_selector now reuses
  cdp.set_checkbox_state instead of duplicating it

Security (PR browser-use#169 feedback):
- extension: type=tel and phone/telefon/gsm naming count as sensitive
  (a real phone number leaked into a saved workflow exactly this way);
  rrweb now masks ALL inputs (nothing downstream consumes rrweb input
  data, so defense-in-depth is free)
- redaction: hints now include elementText/elementTag/inputType, and
  the pattern covers cc-number/cc-csc/cc-exp autocomplete tokens plus
  tel/phone terminology (EN+TR)
- variable identifier: default omission is type-based (password/card/
  SSN/email/phone), not confidence-based - a context-detected password
  at 0.85 confidence was still persisted as a plaintext default; only
  an already-masked capture may surface as a default

13 new tests (tests/test_redaction.py); full suite 140 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gitguardian

gitguardian Bot commented Aug 11, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@Sangaibisi

Copy link
Copy Markdown
Author

Thanks @cubic-dev-ai — all six addressed in aacf1b7:

  • P0 rrweb still carries OTP/card valuesmaskAllInputs: true. Workflow steps never read rrweb input records (only Scroll/Meta events become steps), so masking everything at the rrweb layer is free and closes the channel completely.
  • P1 type=tel not sensitivetel inputs and phone/telefon/gsm naming now mask at capture; a real phone number leaked into a saved workflow exactly this way.
  • P1 redaction misses cc-* autocomplete / generic selectors → the hint pattern now covers cc-number/cc-csc/cc-exp tokens and tel/phone terminology (EN+TR).
  • P1 elementText not consulted → hint fields extended with elementText/elementTag/inputType.
  • P1 context-detected password (0.85) persisted as default → default omission is now type-based (password/credit-card/SSN/email/phone), not confidence-based; only an already-masked capture may surface as a default.
  • 13 unit tests added (tests/test_redaction.py) pinning the masking and default-omission behavior.

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Thanks @cubic-dev-ai — all six addressed in aacf1b7:

  • P0 rrweb still carries OTP/card valuesmaskAllInputs: true. Workflow steps never read rrweb input records (only Scroll/Meta events become steps), so masking everything at the rrweb layer is free and closes the channel completely.
  • P1 type=tel not sensitivetel inputs and phone/telefon/gsm naming now mask at capture; a real phone number leaked into a saved workflow exactly this way.
  • P1 redaction misses cc-* autocomplete / generic selectors → the hint pattern now covers cc-number/cc-csc/cc-exp tokens and tel/phone terminology (EN+TR).
    ...

@Sangaibisi All six fixes line up with what I flagged, and the code matches your descriptions:

  • rrweb maskAllInputs: true — correct call. Since steps only derive from Scroll/Meta events, this closes the rrweb payload channel for free. Keep an eye on the <textarea>/contenteditable case; maskAllInputs covers input elements but not arbitrary rich-text content, though that's outside the P0 scope.
  • type=tel + phone namingisSensitiveField now returns true for type === "tel" and the EN+TR name/label hints. Good.
  • cc-* autocomplete tokens — the redaction regex covers cc-number|cc-csc|cc-exp, and the capture side matches the same tokens. Consistent.
  • elementText/elementTag/inputType hints — added to _HINT_FIELDS, plus the test_element_text_hint_masks test pins it. Two small notes:
    • elementTag and inputType are in the hint list but the regex only fires on words like cvv/tel/phone — an inputType="text" on a masked-value step won't trip it unless another hint does. Fine on its own; just don't expect it to be the sole signal.
    • test_hotel_is_not_tel correctly guards against the tel substring false positive — nice.
  • Type-based default omission — this is the fix that matters most. SENSITIVE_VARIABLE_TYPES covers password/card/SSN/email/phone regardless of confidence, and only an already-masked '********' value surfaces as a default.

One thing I want to flag on the last item: the code is now both type-based and confidence-based, not purely type-based as the message implies. The elif candidate.confidence < 0.95: entry['default'] = candidate.value branch means a non-sensitive high-confidence candidate (≥0.95) with no suggested_default loses its default — before this PR it always got candidate.value. In practice that path is rare (suggested_default is normally set for non-sensitive matches), so the "no behavior change for non-sensitive fields" claim mostly holds, but it's not strictly true. If you want that guarantee airtight, that branch could be narrowed.

Also heads-up: GitGuardian flagged a "Generic Password" at tests/test_redaction.py:63 — that's the 's3cret!'/'4111...' test fixture, a false positive on test data, not a real leak. Worth a one-line suppress/allowlist so the check stays green.

The 13 tests look well-scoped and cover both the masking and default-omission behavior. Nothing blocking from me.

Tip: get faster answers by chatting with cubic’s review copilot in the review UI

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and no new issues found across 4 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Fix all with cubic | Re-trigger cubic

Comment thread workflows/workflow_use/workflow/variable_identifier.py Outdated
Comment thread extension/src/entrypoints/content.ts Outdated
Comment thread workflows/workflow_use/workflow/variable_identifier.py Outdated
Sangaibisi added a commit to Sangaibisi/workflow-use that referenced this pull request Aug 11, 2026
…t discovery

Actions from cubic-dev-ai re-review of PRs browser-use#168/browser-use#169, applied to develop
(same defects exist here), plus real bugs the widened test net caught:

Security (PR browser-use#169 findings, all confirmed valid):
- variable_identifier: sensitivity is now decided by TYPE or NAME/CONTEXT
  hints (shared redaction matcher). A field named "password"/"iban" that
  pattern-matching classified as plain STRING was still getting its raw
  recorded value written to input_schema.default.
- variable_identifier: a masked capture no longer becomes a '********'
  default - defaults are typed verbatim on replay, so it would literally
  enter eight asterisks into the login field. Sensitive inputs get no
  default at all and are forced required.
- redaction + extension isSensitiveField: phone vocabulary now covers
  mobile/mobil/cell(ular)/msisdn/cep naming (fields that are not
  type="tel"); \bcep\b bounded so "Recep" doesn't mask. Extension also
  learns secret/token. New is_sensitive_hint() shared helper.

Real bugs caught by un-hiding the nested suites:
- healing/variable_extractor: VAR:name:value markers embedded in larger
  text (e.g. a URL query ?q=VAR:term:laptop) clobbered the ENTIRE field
  with {term}, destroying the URL; multiple markers kept only the last.
  Markers now replace their own span; a marker owning the whole field
  still consumes it (value may contain spaces: VAR:user_name:John Doe).
- semantic_extractor safeGetLabelText: the previous-sibling fallback
  accepted ANY element with textContent as a "label", so a Cancel button
  adopted the neighboring "Submit Form" text and links adopted the whole
  form's text as their key. Non-LABEL siblings now only label true form
  fields, and only with short text. Verified against a live browser
  (8/8 with RUN_BROWSER_TESTS=1).

Test infrastructure (PR browser-use#168 findings):
- pyproject testpaths widened to tests + workflow_use + the top-level
  regression file: workflow_use/{mcp,workflow,healing,builder}/tests were
  silently undiscovered (and hid the two bugs above).
- Live-LLM suites (test_extract, test_exploration_agent,
  test_workflow_creation, test_generate_workflow) gated behind
  RUN_LLM_TESTS - they crashed collection without API keys.
- test_semantic_extractor rewritten for the CDP surface: set_content
  does not exist; markup loads via data: URLs. Browser classes gated
  behind RUN_BROWSER_TESTS; _normalize_text contract test fixed to the
  real behavior (whitespace collapse, case preserved - lookup folds).
- Password-looking fixture strings replaced with inert values so secret
  scanners don't flag the test file itself.

Suite: 191 passed, 13 skipped (was 159/8 before widening).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 1 findings:
- rrweb events now mask ALL input values (maskAllInputs) - the parallel
  custom-event masking alone still persisted OTP/card values through
  RRWEB_EVENT.
- redaction hint fields extended with elementText (legacy steps may
  carry the only usable hint there).
- semantic replay input logging goes through the shared redactor.

Round 2 findings:
- Sensitivity is decided by TYPE or NAME/CONTEXT hints: a field named
  "password"/"iban" whose value pattern-matched as plain STRING was
  still getting its raw recorded value persisted as a default
  (variable_identifier now consults the shared hint matcher).
- A masked capture no longer becomes a '********' default - defaults are
  typed verbatim on replay, so it would literally enter eight asterisks.
  Sensitive inputs get no default and are forced required.
- Phone vocabulary covers mobile/mobil/cell(ular)/msisdn/cep naming on
  both capture (extension) and replay (redaction) sides - fields that
  are not type="tel"; \bcep\b bounded so "Recep" doesn't mask.
- Test fixture strings replaced with inert non-password-like values so
  secret scanners don't flag the test file itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Sangaibisi
Sangaibisi force-pushed the fix/mask-sensitive-recorded-values branch from aacf1b7 to c075b6d Compare August 11, 2026 11:17
@Sangaibisi

Copy link
Copy Markdown
Author

Round-2 review response (force-pushed c075b6d, replacing aacf1b7):

  • Hint-based sensitivity: variable_identifier now consults the shared redaction matcher, so a field named password/iban whose value pattern-matched as plain STRING no longer gets its raw value persisted as a default.
  • No masked defaults: a captured ******** is no longer written to input_schema.default - defaults are typed verbatim on replay, so it would literally enter eight asterisks into the login field. Sensitive inputs get no default and are forced required.
  • mobile/cell vocabulary: capture (extension) and replay (redaction) sides both cover mobile/mobil/cell(ular)/msisdn/cep naming for phone fields that are not type="tel"; \bcep\b is word-bounded so names like "Recep" don't mask.
  • GitGuardian: the flagged fixture string was a fake test password; the amend replaces it (and hunter2) with inert non-password-like values and rewrites history so the branch never contains them.

Branch checks: tests/test_redaction.py 20 passed; extension tsc --noEmit clean.

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