mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-20 02:17:05 +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
@@ -96,17 +96,19 @@ describe('ccs-browser MCP server - recording and replay', () => {
|
||||
});
|
||||
|
||||
it('cleans up recording state when stop finalization fails', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Broken Stop Recording Page',
|
||||
currentUrl: 'https://example.com/broken-stop-recording',
|
||||
recording: {
|
||||
finalizeError: 'recording finalize failed',
|
||||
},
|
||||
const pages: MockPageState[] = [
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Broken Stop Recording Page',
|
||||
currentUrl: 'https://example.com/broken-stop-recording',
|
||||
recording: {
|
||||
finalizeError: 'recording finalize failed',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const stopResponses = await runMcpRequests(
|
||||
pages,
|
||||
[
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
@@ -120,19 +122,25 @@ describe('ccs-browser MCP server - recording and replay', () => {
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_stop_recording', arguments: {} },
|
||||
},
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1019_3,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_start_recording', arguments: {} },
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
expect(getResponseText(responses.find((message) => message.id === 1019_2))).toContain(
|
||||
expect(getResponseText(stopResponses.find((message) => message.id === 1019_2))).toContain(
|
||||
'recording finalize failed'
|
||||
);
|
||||
expect(getResponseText(responses.find((message) => message.id === 1019_3))).toContain(
|
||||
expect(pages[0].recording?.installed).toBe(false);
|
||||
expect(pages[0].recording?.teardownCalls).toBe(1);
|
||||
|
||||
const restartResponses = await runMcpRequests(pages, [
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1019_3,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_start_recording', arguments: {} },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(getResponseText(restartResponses.find((message) => message.id === 1019_3))).toContain(
|
||||
'status: recording'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { runMcpRequests } from './browser-mcp-test-harness';
|
||||
import type { MockPageState } from './browser-mcp-test-harness';
|
||||
|
||||
describe('ccs-browser MCP server - recording security', () => {
|
||||
it('filters sensitive text targets and printable key presses in injected recorder code', () => {
|
||||
const source = readFileSync('lib/mcp/ccs-browser-server.cjs', 'utf8');
|
||||
const sensitiveAttributePatternMatch = source.match(
|
||||
/const sensitiveAttributePattern = \/(.+)\/i;/
|
||||
);
|
||||
|
||||
expect(source).toContain("sensitiveInputTypes = new Set(['password', 'hidden'])");
|
||||
expect(source).toContain('sensitiveAutocompleteValues');
|
||||
expect(source).toContain('autocompleteTokens.some((token) => sensitiveAutocompleteValues.has(token))');
|
||||
expect(source).toContain('isSensitiveTextTarget(target)');
|
||||
expect(source).toContain("typeof event.key !== 'string' || event.key.length !== 1");
|
||||
expect(sensitiveAttributePatternMatch).not.toBeNull();
|
||||
|
||||
const sensitiveAttributePattern = new RegExp(sensitiveAttributePatternMatch![1], 'i');
|
||||
expect(sensitiveAttributePattern.test('verification_code')).toBe(true);
|
||||
expect(sensitiveAttributePattern.test('security-code')).toBe(true);
|
||||
expect(sensitiveAttributePattern.test('pin')).toBe(true);
|
||||
expect(sensitiveAttributePattern.test('auth_token')).toBe(true);
|
||||
expect(sensitiveAttributePattern.test('author')).toBe(false);
|
||||
});
|
||||
|
||||
it('tears down page recording hooks when stopping or clearing', async () => {
|
||||
const pages: MockPageState[] = [
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Recording Page',
|
||||
currentUrl: 'https://example.com/recording',
|
||||
recording: {
|
||||
events: [{ kind: 'click', selector: '#submit', timestamp: 1710000000000 }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await runMcpRequests(pages, [
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_start_recording', arguments: {} },
|
||||
},
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_stop_recording', arguments: {} },
|
||||
},
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 3,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_clear_recording', arguments: {} },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(pages[0].recording?.installed).toBe(false);
|
||||
expect(pages[0].recording?.teardownCalls).toBe(1);
|
||||
|
||||
await runMcpRequests(pages, [
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 4,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_start_recording', arguments: {} },
|
||||
},
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 5,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_clear_recording', arguments: {} },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(pages[0].recording?.installed).toBe(false);
|
||||
expect(pages[0].recording?.teardownCalls).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -207,6 +207,8 @@ type MockRecordingPlan = {
|
||||
warnings?: MockRecordingWarning[];
|
||||
injectionError?: string;
|
||||
finalizeError?: string;
|
||||
installed?: boolean;
|
||||
teardownCalls?: number;
|
||||
};
|
||||
|
||||
type MockFrameState = {
|
||||
@@ -1174,6 +1176,10 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (plan.installed) {
|
||||
plan.teardownCalls = (plan.teardownCalls || 0) + 1;
|
||||
}
|
||||
plan.installed = true;
|
||||
if (
|
||||
recordingPayload &&
|
||||
(recordingPayload.events?.length || recordingPayload.warnings?.length)
|
||||
@@ -1187,9 +1193,8 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
||||
}
|
||||
|
||||
if (
|
||||
expression.includes(
|
||||
'globalThis.__CCS_BROWSER_RECORDING_RECORDER__ || { events: [], warnings: [] }'
|
||||
)
|
||||
expression.includes('const recorder = globalThis.__CCS_BROWSER_RECORDING_RECORDER__') &&
|
||||
expression.includes('return { events, warnings }')
|
||||
) {
|
||||
const plan = getMockRecordingPlan(page);
|
||||
if (plan.finalizeError) {
|
||||
@@ -1201,6 +1206,10 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (plan.installed) {
|
||||
plan.teardownCalls = (plan.teardownCalls || 0) + 1;
|
||||
}
|
||||
plan.installed = false;
|
||||
reply({
|
||||
result: {
|
||||
type: 'object',
|
||||
@@ -1213,6 +1222,19 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
expression.includes('const recorder = globalThis.__CCS_BROWSER_RECORDING_RECORDER__') &&
|
||||
expression.includes('return { installed: false }')
|
||||
) {
|
||||
const plan = getMockRecordingPlan(page);
|
||||
if (plan.installed) {
|
||||
plan.teardownCalls = (plan.teardownCalls || 0) + 1;
|
||||
}
|
||||
plan.installed = false;
|
||||
reply({ result: { type: 'object', value: { installed: false } } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (expression.includes('new DragEvent') && expression.includes('new DataTransfer()')) {
|
||||
const selector = parseJsonArgument(expression, 'selector') || '';
|
||||
const nth = parseNumberArgument(expression, 'nth') ?? 0;
|
||||
|
||||
Reference in new issue
Block a user