mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 08:16:50 +00:00
fix(browser): stop recorder leaking sensitive input
Squash merge PR #1293 into dev.
This commit is contained in:
1 parent
6021e1cf17
commit
8051f1614b
4 files changed
+229
-23
No files matched your search
@@ -5492,7 +5492,10 @@ function buildRecordingInstallExpression(recordingPayload) {
|
||||
const recordingPayload = JSON.parse(${JSON.stringify(JSON.stringify(recordingPayload))});
|
||||
const existing = globalThis.__CCS_BROWSER_RECORDING_RECORDER__;
|
||||
if (existing && existing.installed === true) {
|
||||
return { installed: true };
|
||||
if (typeof existing.teardown === 'function') {
|
||||
existing.teardown();
|
||||
}
|
||||
delete globalThis.__CCS_BROWSER_RECORDING_RECORDER__;
|
||||
}
|
||||
|
||||
const events = Array.isArray(recordingPayload.events) ? [...recordingPayload.events] : [];
|
||||
@@ -5524,8 +5527,42 @@ function buildRecordingInstallExpression(recordingPayload) {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
const sensitiveInputTypes = new Set(['password', 'hidden']);
|
||||
const sensitiveAttributePattern = /(?:pass(?:word|code|phrase)?|pwd|secret|token|api[-_ ]?key|access[-_ ]?key|private[-_ ]?key|credential|otp|one[-_ ]?time[-_ ]?(?:code|password)|verif(?:ication)?[-_ ]?code|security[-_ ]?code|pin|auth(?:orization)?[-_ ]?(?:code|token)|mfa|2fa)/i;
|
||||
const sensitiveAutocompleteValues = new Set([
|
||||
'current-password',
|
||||
'new-password',
|
||||
'one-time-code',
|
||||
'cc-number',
|
||||
'cc-csc',
|
||||
]);
|
||||
const isSensitiveTextTarget = (target) => {
|
||||
if (!target || !(target instanceof Element)) {
|
||||
return false;
|
||||
}
|
||||
if (target instanceof HTMLInputElement) {
|
||||
const inputType = String(target.type || '').toLowerCase();
|
||||
if (sensitiveInputTypes.has(inputType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const autocompleteTokens = String(target.getAttribute('autocomplete') || '')
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
if (autocompleteTokens.some((token) => sensitiveAutocompleteValues.has(token))) {
|
||||
return true;
|
||||
}
|
||||
const attributesToInspect = ['id', 'name', 'placeholder', 'aria-label', 'data-testid'];
|
||||
return attributesToInspect.some((attributeName) =>
|
||||
sensitiveAttributePattern.test(String(target.getAttribute(attributeName) || ''))
|
||||
);
|
||||
};
|
||||
const onInput = (event) => {
|
||||
const target = event.target;
|
||||
if (isSensitiveTextTarget(target)) {
|
||||
return;
|
||||
}
|
||||
let text = '';
|
||||
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
||||
text = target.value;
|
||||
@@ -5536,7 +5573,19 @@ function buildRecordingInstallExpression(recordingPayload) {
|
||||
}
|
||||
pushEvent({ kind: 'type', selector: getSelector(target), text, timestamp: Date.now() });
|
||||
};
|
||||
const isRecordableKey = (event) => {
|
||||
if (isSensitiveTextTarget(event.target)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof event.key !== 'string' || event.key.length !== 1) {
|
||||
return true;
|
||||
}
|
||||
return event.altKey === true || event.ctrlKey === true || event.metaKey === true;
|
||||
};
|
||||
const onKeyDown = (event) => {
|
||||
if (!isRecordableKey(event)) {
|
||||
return;
|
||||
}
|
||||
const modifiers = [];
|
||||
if (event.altKey) modifiers.push('Alt');
|
||||
if (event.ctrlKey) modifiers.push('Control');
|
||||
@@ -5604,7 +5653,19 @@ async function finalizeRecordingCapture(session) {
|
||||
};
|
||||
|
||||
const response = await sendCdpCommand(page, 'Runtime.evaluate', {
|
||||
expression: `(() => globalThis.__CCS_BROWSER_RECORDING_RECORDER__ || { events: [], warnings: [] })()`,
|
||||
expression: `(() => {
|
||||
const recorder = globalThis.__CCS_BROWSER_RECORDING_RECORDER__;
|
||||
if (!recorder) {
|
||||
return { events: [], warnings: [] };
|
||||
}
|
||||
const events = Array.isArray(recorder.events) ? [...recorder.events] : [];
|
||||
const warnings = Array.isArray(recorder.warnings) ? [...recorder.warnings] : [];
|
||||
if (typeof recorder.teardown === 'function') {
|
||||
recorder.teardown();
|
||||
}
|
||||
delete globalThis.__CCS_BROWSER_RECORDING_RECORDER__;
|
||||
return { events, warnings };
|
||||
})()`,
|
||||
returnByValue: true,
|
||||
awaitPromise: true,
|
||||
});
|
||||
@@ -5624,6 +5685,28 @@ async function finalizeRecordingCapture(session) {
|
||||
session.warnings = warnings.map((warning) => String(warning.message || warning));
|
||||
}
|
||||
|
||||
async function teardownRecordingCapture(session) {
|
||||
if (!session || !session.pageWebSocketDebuggerUrl) {
|
||||
return;
|
||||
}
|
||||
const page = {
|
||||
id: session.pageId,
|
||||
webSocketDebuggerUrl: session.pageWebSocketDebuggerUrl,
|
||||
};
|
||||
await sendCdpCommand(page, 'Runtime.evaluate', {
|
||||
expression: `(() => {
|
||||
const recorder = globalThis.__CCS_BROWSER_RECORDING_RECORDER__;
|
||||
if (recorder && typeof recorder.teardown === 'function') {
|
||||
recorder.teardown();
|
||||
}
|
||||
delete globalThis.__CCS_BROWSER_RECORDING_RECORDER__;
|
||||
return { installed: false };
|
||||
})()`,
|
||||
returnByValue: true,
|
||||
awaitPromise: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleStartRecording(toolArgs) {
|
||||
if (activeRecordingSession) {
|
||||
throw new Error('recording already active');
|
||||
@@ -5674,6 +5757,11 @@ async function handleStopRecording() {
|
||||
} catch (error) {
|
||||
finalizeError = error instanceof Error ? error : new Error(String(error));
|
||||
session.warnings.push(`recording capture finalization failed: ${finalizeError.message}`);
|
||||
try {
|
||||
await teardownRecordingCapture(session);
|
||||
} catch {
|
||||
// Preserve the original finalization failure for the caller.
|
||||
}
|
||||
}
|
||||
session.status = 'stopped';
|
||||
session.stoppedAt = new Date().toISOString();
|
||||
@@ -5694,6 +5782,12 @@ async function handleClearRecording() {
|
||||
if (!latestRecordingSession && !activeRecordingSession) {
|
||||
throw new Error('no recording available');
|
||||
}
|
||||
const session = activeRecordingSession || latestRecordingSession;
|
||||
try {
|
||||
await teardownRecordingCapture(session);
|
||||
} catch {
|
||||
// Clearing session-local recording state should still succeed if the page is already gone.
|
||||
}
|
||||
activeRecordingSession = null;
|
||||
latestRecordingSession = null;
|
||||
return 'status: cleared';
|
||||
|
||||
Reference in new issue
Block a user