diff --git a/lib/mcp/ccs-browser-server.cjs b/lib/mcp/ccs-browser-server.cjs index a69c413f..96ea4cb5 100755 --- a/lib/mcp/ccs-browser-server.cjs +++ b/lib/mcp/ccs-browser-server.cjs @@ -159,6 +159,8 @@ const DEFAULT_WAIT_TIMEOUT_MS = 2000; const DEFAULT_WAIT_POLL_INTERVAL_MS = 100; const DEFAULT_DRAG_STEPS = 5; const MAX_POINTER_ACTIONS = 25; +const MAX_CLICK_COUNT = 25; +const MAX_KEY_REPEAT = 25; const SESSION_START_SETTLE_WINDOW_MS = 250; const MAX_ARTIFACT_FILE_BYTES = 5 * 1024 * 1024; const MAX_LOCAL_TRANSFER_FILE_BYTES = 10 * 1024 * 1024; @@ -454,6 +456,7 @@ function getTools() { clickCount: { type: 'integer', minimum: 1, + maximum: MAX_CLICK_COUNT, description: 'Optional click count. Defaults to 1.', }, }, @@ -519,6 +522,7 @@ function getTools() { repeat: { type: 'integer', minimum: 1, + maximum: MAX_KEY_REPEAT, description: 'Optional repeat count. Defaults to 1.', }, }, @@ -2305,10 +2309,13 @@ function requirePositiveIntegerOrDefault(value, label, fallback) { return value; } -function requirePositiveInteger(value, label) { +function requirePositiveInteger(value, label, maximum = undefined) { if (!Number.isInteger(value) || value <= 0) { throw new Error(`${label} must be a positive integer`); } + if (maximum !== undefined && value > maximum) { + throw new Error(`${label} must be less than or equal to ${maximum}`); + } return value; } @@ -3555,7 +3562,7 @@ async function handleClick(toolArgs) { const clickCount = toolArgs.clickCount === undefined ? 1 - : requirePositiveInteger(toolArgs.clickCount, 'clickCount'); + : requirePositiveInteger(toolArgs.clickCount, 'clickCount', MAX_CLICK_COUNT); const expression = `(() => { const selector = JSON.parse(${JSON.stringify(JSON.stringify(selector))}); @@ -3805,7 +3812,9 @@ async function handlePressKey(toolArgs) { 'Shift', ]); const repeat = - toolArgs.repeat === undefined ? 1 : requirePositiveInteger(toolArgs.repeat, 'repeat'); + toolArgs.repeat === undefined + ? 1 + : requirePositiveInteger(toolArgs.repeat, 'repeat', MAX_KEY_REPEAT); const modifierMask = (modifiers.includes('Alt') ? 1 : 0) | (modifiers.includes('Control') ? 2 : 0) | diff --git a/tests/unit/hooks/browser-mcp-navigation-and-query.test.ts b/tests/unit/hooks/browser-mcp-navigation-and-query.test.ts index bbd09250..5e38d7e8 100644 --- a/tests/unit/hooks/browser-mcp-navigation-and-query.test.ts +++ b/tests/unit/hooks/browser-mcp-navigation-and-query.test.ts @@ -722,6 +722,47 @@ describe('ccs-browser MCP server - navigation and query', () => { ); }); + + it('rejects excessive clickCount and repeat values', async () => { + const responses = await runMcpRequests( + [{ id: 'page-1', title: 'Limits Page', currentUrl: 'https://example.com/' }], + [ + { + jsonrpc: '2.0', + id: 713, + method: 'tools/call', + params: { + name: 'browser_click', + arguments: { + selector: '#submit', + clickCount: 26, + }, + }, + }, + { + jsonrpc: '2.0', + id: 714, + method: 'tools/call', + params: { + name: 'browser_press_key', + arguments: { + key: 'k', + repeat: 26, + }, + }, + }, + ] + ); + + const clickResponse = responses.find((message) => message.id === 713); + expect((clickResponse?.result as { isError?: boolean }).isError).toBe(true); + expect(getResponseText(clickResponse)).toContain('clickCount must be less than or equal to 25'); + + const keyResponse = responses.find((message) => message.id === 714); + expect((keyResponse?.result as { isError?: boolean }).isError).toBe(true); + expect(getResponseText(keyResponse)).toContain('repeat must be less than or equal to 25'); + }); + it('scrolls an element into view with browser_scroll', async () => { const responses = await runMcpRequests( [ diff --git a/tests/unit/hooks/browser-mcp-session-and-intercepts.test.ts b/tests/unit/hooks/browser-mcp-session-and-intercepts.test.ts index 7f160f9e..6c2aac6a 100644 --- a/tests/unit/hooks/browser-mcp-session-and-intercepts.test.ts +++ b/tests/unit/hooks/browser-mcp-session-and-intercepts.test.ts @@ -79,11 +79,17 @@ describe('ccs-browser MCP server - session and interception', () => { expect(clickTool?.inputSchema?.properties?.clickCount).toMatchObject({ type: 'integer', minimum: 1, + maximum: 25, }); const keyTool = tools.find((tool) => tool.name === 'browser_press_key'); expect(keyTool?.inputSchema?.properties?.key).toMatchObject({ type: 'string' }); expect(keyTool?.inputSchema?.properties?.modifiers).toMatchObject({ type: 'array' }); + expect(keyTool?.inputSchema?.properties?.repeat).toMatchObject({ + type: 'integer', + minimum: 1, + maximum: 25, + }); const scrollTool = tools.find((tool) => tool.name === 'browser_scroll'); expect(scrollTool?.inputSchema?.properties?.deltaX).toMatchObject({ type: 'number' });