-
Notifications
You must be signed in to change notification settings - Fork 347
fix(security): stop credential/PII leaks at capture, persistence and replay #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| ScrollDeterministicAction, | ||
| SelectDropdownOptionDeterministicAction, | ||
| ) | ||
| from workflow_use.workflow.redaction import redact_step_value | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -138,7 +139,8 @@ async def input( | |
| await locator.click(force=True) | ||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| msg = f'⌨️ Input "{logged_value}" into element with CSS selector: {truncate_selector(selector_used)} (original: {truncate_selector(original_selector)})' | ||
| logger.info(msg) | ||
| return ActionResult(extracted_content=msg, include_in_memory=True) | ||
| except Exception as e: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| """Log/report redaction for sensitive step values. | ||
|
|
||
| Recorded workflows can carry credentials and PII in step values (login flows, | ||
| card forms). Whatever the capture side does, the replay side must not re-leak | ||
| them through INFO logs or error reports. | ||
| """ | ||
|
|
||
| import re | ||
|
|
||
| # Field hints that mark a value as sensitive for logging/error-report purposes | ||
| _SENSITIVE_HINT_RE = re.compile( | ||
| r'(password|passwd|pwd|otp\b|one.?time|verification|security.?code|cvv|cvc|card.?number|kart|ssn\b|social.?security|tckn|kimlik|iban|secret|token)', | ||
| re.IGNORECASE, | ||
| ) | ||
| VALUE_MASK = '********' | ||
|
|
||
|
|
||
| def redact_step_value(step, value): | ||
| """Mask a step value in logs/error reports when the field looks sensitive. | ||
|
|
||
| *step* may be a workflow step or an action params object - any attribute bag | ||
| with target_text/description/cssSelector-style fields. | ||
| """ | ||
| if value is None: | ||
| return None | ||
| text = str(value) | ||
| if text == VALUE_MASK: | ||
| return text | ||
| hints = ' '.join( | ||
| str(getattr(step, field, '') or '') for field in ('target_text', 'targetText', 'description', 'cssSelector', 'xpath') | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| ) | ||
| return VALUE_MASK if _SENSITIVE_HINT_RE.search(hints) else text | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import asyncio | ||
| import json | ||
| import logging | ||
| import re | ||
| import traceback | ||
| from typing import TYPE_CHECKING, Dict, List, Optional, Tuple | ||
|
|
||
|
|
@@ -22,6 +23,7 @@ | |
| WorkflowStep, | ||
| ) | ||
| from workflow_use.workflow.error_reporter import ErrorCategory, ErrorContext, ErrorReporter | ||
| from workflow_use.workflow.redaction import redact_step_value | ||
| from workflow_use.workflow.semantic_extractor import SemanticExtractor | ||
| from workflow_use.workflow.step_verifier import StepVerifier, VerificationResult | ||
|
|
||
|
|
@@ -368,7 +370,6 @@ def _find_element_by_pattern( | |
| Returns: | ||
| Element info dict if found, None otherwise | ||
| """ | ||
| import re | ||
|
|
||
| logger.info(f"Finding element by pattern: '{pattern}' (position: {position_hint}, container: {container_hint})") | ||
|
|
||
|
|
@@ -1531,7 +1532,7 @@ async def input_executor(): | |
| # Click removed - not needed after fill and CDP doesn't support force parameter | ||
| 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}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Replay of a recorded telephone or Prompt for AI agents |
||
| logger.info(msg) | ||
| return ActionResult(extracted_content=msg, include_in_memory=True) | ||
|
|
||
|
|
@@ -2098,7 +2099,7 @@ async def _execute_with_verification_and_retry(self, step_executor, step, verifi | |
| consecutive_verification_failures=self.consecutive_verification_failures, | ||
| retry_attempts=self.max_retries + 1, | ||
| target_text=getattr(step, 'target_text', None), | ||
| input_value=getattr(step, 'value', None), | ||
| input_value=redact_step_value(step, getattr(step, 'value', None)), | ||
| last_successful_step=self.last_successful_step, | ||
| current_url=current_url, | ||
| page_title=page_title, | ||
|
|
@@ -3130,7 +3131,6 @@ def _date_matches(self, target_date: str, element_date: str) -> bool: | |
|
|
||
| def _normalize_date(self, date_str: str) -> str: | ||
| """Normalize date string to YYYY-MM-DD format.""" | ||
| import re | ||
| from datetime import datetime | ||
|
|
||
| # Remove extra whitespace and common words | ||
|
|
@@ -3207,7 +3207,6 @@ def _score_flight_option(self, criteria: Dict, context: Dict, text: str) -> int: | |
|
|
||
| def _price_in_range(self, price_str: str, price_range: str) -> bool: | ||
| """Check if price falls within specified range.""" | ||
| import re | ||
|
|
||
| try: | ||
| # Extract numeric price | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.