Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions extension/src/entrypoints/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,33 @@ function stopRecorder() {
}

// --- Helper function to extract semantic information ---
const SENSITIVE_VALUE_MASK = "********";

// Broader than type=password: OTP fields are type=text/tel with
// autocomplete=one-time-code, card/CVV fields are type=text/number, and many
// sites only reveal sensitivity through name/id/label conventions.
function isSensitiveField(element: HTMLElement): boolean {
const el = element as HTMLInputElement;
const type = (el.type || "").toLowerCase();
if (type === "password") return true;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (el.tagName.toLowerCase() !== "input" && el.tagName.toLowerCase() !== "textarea") return false;
const autocomplete = (el.getAttribute("autocomplete") || "").toLowerCase();
if (/(one-time-code|cc-number|cc-csc|cc-exp|new-password|current-password)/.test(autocomplete)) {
return true;
}
const hints = [
el.name || "",
el.id || "",
el.getAttribute("aria-label") || "",
(el as HTMLInputElement).placeholder || "",
]
.join(" ")
.toLowerCase();
return /(password|passwd|pwd|otp\b|one.?time|verification.?code|security.?code|cvv|cvc|csc\b|card.?number|kart.?no|ssn\b|social.?security|tckn|tc.?kimlik|iban)/.test(
hints
);
}

function extractSemanticInfo(element: HTMLElement) {
// Get associated label text using multiple strategies
let labelText = '';
Expand Down Expand Up @@ -531,15 +558,20 @@ function extractSemanticInfo(element: HTMLElement) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const radioButtonInfo = (element as any)._radioButtonInfo || null;

// NEVER carry the raw value of a sensitive field: semanticInfo is embedded in
// stored events and shipped to the server, so an unmasked value here leaked
// real passwords even while the step's own value field was masked.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawValue = (element as any).value || "";

return {
labelText,
textContent: element.textContent?.trim().slice(0, 200) || "",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
placeholder: (element as any).placeholder || "",
title: element.title || "",
ariaLabel: element.getAttribute('aria-label') || "",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: (element as any).value || "",
value: rawValue && isSensitiveField(element) ? SENSITIVE_VALUE_MASK : rawValue,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name: (element as any).name || "",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -633,7 +665,7 @@ function handleCustomClick(event: MouseEvent) {
// Enhanced radio button information
radioButtonInfo: semanticInfo.radioButtonInfo,
};
console.log("Sending CUSTOM_CLICK_EVENT:", clickData);
console.log("Sending CUSTOM_CLICK_EVENT");
chrome.runtime.sendMessage({
type: "CUSTOM_CLICK_EVENT",
payload: clickData,
Expand Down Expand Up @@ -689,7 +721,8 @@ function handleInput(event: Event) {
if (!isRecordingActive) return;
const targetElement = event.target as HTMLInputElement | HTMLTextAreaElement;
if (!targetElement || !("value" in targetElement)) return;
const isPassword = targetElement.type === "password";
// Mask anything sensitive, not just type=password (OTP, card, CVV, SSN, ...)
const isSensitive = isSensitiveField(targetElement as HTMLElement);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

try {
const xpath = getXPath(targetElement);
Expand All @@ -711,14 +744,14 @@ function handleInput(event: Event) {
xpath: xpath,
cssSelector: getEnhancedCSSSelector(targetElement, xpath),
elementTag: targetElement.tagName,
value: isPassword ? "********" : targetElement.value,
value: isSensitive ? SENSITIVE_VALUE_MASK : targetElement.value,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputType: (targetElement as any).type?.toLowerCase() || 'text', // Input type (text, password, email, etc.)
// Semantic information for target_text based workflows
targetText: targetText,
semanticInfo: semanticInfo,
};
console.log("Sending CUSTOM_INPUT_EVENT:", inputData);
console.log("Sending CUSTOM_INPUT_EVENT");
chrome.runtime.sendMessage({
type: "CUSTOM_INPUT_EVENT",
payload: inputData,
Expand Down Expand Up @@ -769,7 +802,7 @@ function handleSelectChange(event: Event) {
targetText: semanticInfo.labelText || fieldName,
semanticInfo: semanticInfo
};
console.log("Sending CUSTOM_SELECT_EVENT:", selectData);
console.log("Sending CUSTOM_SELECT_EVENT");
chrome.runtime.sendMessage({
type: "CUSTOM_SELECT_EVENT",
payload: selectData,
Expand Down Expand Up @@ -845,7 +878,7 @@ function handleKeydown(event: KeyboardEvent) {
cssSelector: cssSelector, // CSS selector of the element in focus (if any)
elementTag: elementTag, // Tag name of the element in focus
};
console.log("Sending CUSTOM_KEY_EVENT:", keyData);
console.log("Sending CUSTOM_KEY_EVENT");
chrome.runtime.sendMessage({
type: "CUSTOM_KEY_EVENT",
payload: keyData,
Expand Down
4 changes: 3 additions & 1 deletion workflows/workflow_use/controller/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ScrollDeterministicAction,
SelectDropdownOptionDeterministicAction,
)
from workflow_use.workflow.redaction import redact_step_value

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -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)

@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

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:
Expand Down
32 changes: 32 additions & 0 deletions workflows/workflow_use/workflow/redaction.py
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')
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
)
return VALUE_MASK if _SENSITIVE_HINT_RE.search(hints) else text
9 changes: 4 additions & 5 deletions workflows/workflow_use/workflow/semantic_executor.py
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

Expand All @@ -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

Expand Down Expand Up @@ -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})")

Expand Down Expand Up @@ -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}"

@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

logger.info(msg)
return ActionResult(extracted_content=msg, include_in_memory=True)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions workflows/workflow_use/workflow/variable_identifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,12 +491,15 @@ def _generate_input_schema(self, variables: Dict[str, VariableCandidate]) -> Lis
if candidate.description:
entry['description'] = candidate.description

# IMPORTANT: Always add default value (original value from workflow)
# This allows the workflow to run without user input if desired
if candidate.suggested_default:
# Add a default so the workflow can run without user input - EXCEPT for
# high-confidence sensitive matches (SSN, credit card, password, ...),
# where suggested_default is deliberately None: persisting the recorded
# value as a plaintext default would write the secret into the saved
# .workflow.yaml on disk.
if candidate.suggested_default is not None:
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
entry['default'] = candidate.suggested_default
else:
# If no suggested default, use the original value
elif candidate.confidence < 0.95:
# Low-confidence candidate without an explicit suggestion
entry['default'] = candidate.value

schema.append(entry)
Expand Down