feat: extend --no-javascript-evaluation to cover navigations and initScripts (#2638)

This commit is contained in:
Alex Rudenko
2026-09-02 14:00:51 +00:00
committed by GitHub
parent 24e3e7c669
commit 4993a0f40f
9 changed files with 454 additions and 48 deletions
+1 -1
View File
@@ -181,7 +181,7 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Default:** `true`
- **`--javascriptEvaluation`/ `--javascript-evaluation`**
Set to false to disable tools to evaluate JavaScript scripts.
Set to false to disable JavaScript execution. When disabled, evaluation tools (evaluate_script and slim evaluate) are disabled, the initScript parameter in navigate_page is turned off, and navigating to javascript:, data:, or vbscript: URLs is disallowed.
- **Type:** boolean
- **Default:** `true`
+3 -2
View File
@@ -300,7 +300,8 @@ export const mcpOptions = {
javascriptEvaluation: {
type: 'boolean',
default: true,
describe: 'Set to false to disable tools to evaluate JavaScript scripts.',
describe:
'Set to false to disable JavaScript execution. When disabled, evaluation tools (evaluate_script and slim evaluate) are disabled, the initScript parameter in navigate_page is turned off, and navigating to javascript:, data:, or vbscript: URLs is disallowed.',
},
clearcutEndpoint: {
type: 'string',
@@ -562,7 +563,7 @@ export function parser(
],
[
'$0 --no-javascript-evaluation',
'Disable tools to evaluate JavaScript scripts.',
'Disable JavaScript execution (disables evaluation tools, initScript in navigate_page, and navigating to javascript:, data:, or vbscript: URLs).',
],
[
'$0 --slim',
+19 -8
View File
@@ -7,6 +7,7 @@
import type {CdpPage} from '../third_party/index.js';
import {zod} from '../third_party/index.js';
import {logger} from '../utils/logger.js';
import {validateUrl} from '../utils/url.js';
import {ToolCategory} from './categories.js';
import {
@@ -96,7 +97,7 @@ export const closePage = defineTool({
},
});
export const newPage = defineTool(() => {
export const newPage = defineTool(args => {
return {
name: 'new_page',
description: `Open a new tab and load a URL. Use project URL if not specified otherwise.`,
@@ -125,6 +126,8 @@ export const newPage = defineTool(() => {
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response, context) => {
validateUrl(request.params.url, args?.javascriptEvaluation);
const page = await context.newPage(
request.params.background,
request.params.isolatedContext,
@@ -145,7 +148,7 @@ export const newPage = defineTool(() => {
};
});
export const navigatePage = definePageTool(() => {
export const navigatePage = definePageTool(args => {
return {
name: 'navigate_page',
description: `Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.`,
@@ -171,12 +174,16 @@ export const navigatePage = definePageTool(() => {
.describe(
'Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.',
),
initScript: zod
.string()
.optional()
.describe(
'A JavaScript script to be executed on each new document before any other scripts for the next navigation.',
),
...(args?.javascriptEvaluation !== false
? {
initScript: zod
.string()
.optional()
.describe(
'A JavaScript script to be executed on each new document before any other scripts for the next navigation.',
),
}
: {}),
...timeoutSchema,
},
blockedByDialog: false,
@@ -195,6 +202,10 @@ export const navigatePage = definePageTool(() => {
request.params.type = 'url';
}
if (request.params.url) {
validateUrl(request.params.url, args?.javascriptEvaluation);
}
let initScriptId: string | undefined;
if (request.params.initScript) {
const {identifier} = await page.pptrPage.evaluateOnNewDocument(
+39 -34
View File
@@ -8,6 +8,7 @@ import type {Dialog} from '../../third_party/index.js';
import {zod} from '../../third_party/index.js';
import {ToolCategory} from '../categories.js';
import {definePageTool} from '../ToolDefinition.js';
import {validateUrl} from '../../utils/url.js';
export const screenshot = definePageTool({
name: 'screenshot',
@@ -34,43 +35,47 @@ export const screenshot = definePageTool({
},
});
export const navigate = definePageTool({
name: 'navigate',
description: `Loads a URL`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: false,
},
schema: {
url: zod.string().describe('URL to navigate to'),
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response) => {
const page = request.page;
export const navigate = definePageTool(args => {
return {
name: 'navigate',
description: `Loads a URL`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: false,
},
schema: {
url: zod.string().describe('URL to navigate to'),
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response) => {
validateUrl(request.params.url, args?.javascriptEvaluation);
const options = {
timeout: 30_000,
};
const page = request.page;
const dialogHandler = (dialog: Dialog) => {
if (dialog.type() === 'beforeunload') {
response.appendResponseLine(`Accepted a beforeunload dialog.`);
void dialog.accept();
// We are not going to report the dialog like regular dialogs.
page.clearDialog();
const options = {
timeout: 30_000,
};
const dialogHandler = (dialog: Dialog) => {
if (dialog.type() === 'beforeunload') {
response.appendResponseLine(`Accepted a beforeunload dialog.`);
void dialog.accept();
// We are not going to report the dialog like regular dialogs.
page.clearDialog();
}
};
page.pptrPage.on('dialog', dialogHandler);
try {
await page.pptrPage.goto(request.params.url, options);
response.appendResponseLine(`Navigated to ${page.pptrPage.url()}.`);
} finally {
page.pptrPage.off('dialog', dialogHandler);
}
};
page.pptrPage.on('dialog', dialogHandler);
try {
await page.pptrPage.goto(request.params.url, options);
response.appendResponseLine(`Navigated to ${page.pptrPage.url()}.`);
} finally {
page.pptrPage.off('dialog', dialogHandler);
}
},
},
};
});
export const evaluate = definePageTool({
+32
View File
@@ -48,3 +48,35 @@ export function isLocalhost(url?: string): boolean {
return false;
}
const DISALLOWED_PROTOCOLS = new Set(['javascript:', 'data:', 'vbscript:']);
/**
* Validates a URL string by parsing it with `new URL` and checking for disallowed protocols.
*
* @param url The URL string to validate.
* @param javascriptEvaluation Whether JavaScript evaluation is enabled.
* @returns The parsed URL.
* @throws Error if the URL does not parse with `new URL`, or if JavaScript evaluation is disabled and a disallowed URL is passed.
*/
export function validateUrl(url: string, javascriptEvaluation?: boolean): URL {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
throw new Error(
`Invalid URL: "${url}". URLs must be valid according to the URL standard.`,
);
}
if (
javascriptEvaluation === false &&
DISALLOWED_PROTOCOLS.has(parsed.protocol)
) {
throw new Error(
`Navigating to ${parsed.protocol} URLs is not allowed when JavaScript evaluation is disabled.`,
);
}
return parsed;
}
@@ -124,6 +124,30 @@ describe('chrome-devtools', () => {
),
'restart command suggestion is missing: ' + result.stdout,
);
const navResult = await runCli(
['navigate_page', '1', '--url', 'javascript:alert(1)'],
sessionId,
);
assert.strictEqual(navResult.status, 0);
assert(
navResult.stdout.includes(
'Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled.',
),
'error message is unexpected: ' + navResult.stdout,
);
const initScriptResult = await runCli(
['navigate_page', '1', '--initScript', 'alert(1)'],
sessionId,
);
assert.strictEqual(initScriptResult.status, 0);
assert(
initScriptResult.stdout.includes(
'Unknown argument for tool "navigate_page": "initScript"',
),
'error message is unexpected: ' + initScriptResult.stdout,
);
});
it('can record a performance trace', async () => {
+173 -1
View File
@@ -11,7 +11,10 @@ import {afterEach, describe, it} from 'node:test';
import type {Dialog} from 'puppeteer-core';
import sinon from 'sinon';
import type {ParsedArguments} from '../../src/config/mcp-options.js';
import {
parseArguments,
type ParsedArguments,
} from '../../src/config/mcp-options.js';
import {
listPages,
newPage,
@@ -249,6 +252,73 @@ describe('pages', () => {
assert.ok(response.includePages);
});
});
it('throws when navigating to a javascript URL and javascriptEvaluation is false', async () => {
await withMcpContext(async (response, context) => {
const disabledArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--no-javascript-evaluation'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const tool = newPage(disabledArgs);
await assert.rejects(
async () => {
await tool.handler(
{params: {url: 'javascript:alert(1)'}},
response,
context,
);
},
{
message:
'Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{params: {url: 'data:text/html,<div>test</div>'}},
response,
context,
);
},
{
message:
'Navigating to data: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{params: {url: 'vbscript:msgbox(1)'}},
response,
context,
);
},
{
message:
'Navigating to vbscript: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
});
});
it('throws when URL does not parse with new URL', async () => {
await withMcpContext(async (response, context) => {
const tool = newPage();
await assert.rejects(
async () => {
await tool.handler(
{params: {url: 'not a valid url'}},
response,
context,
);
},
{
message:
'Invalid URL: "not a valid url". URLs must be valid according to the URL standard.',
},
);
});
});
it('create a page in the background', async () => {
await withMcpContext(async (response, context) => {
const originalPage = context.getPageById(1);
@@ -917,6 +987,108 @@ describe('pages', () => {
});
});
it('omits initScript from schema when javascriptEvaluation is false', () => {
const defaultTool = navigatePage();
assert.strictEqual('initScript' in defaultTool.schema, true);
const disabledArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--no-javascript-evaluation'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const disabledTool = navigatePage(disabledArgs);
assert.strictEqual('initScript' in disabledTool.schema, false);
});
it('throws when navigating to a javascript, data, or vbscript URL and javascriptEvaluation is false', async () => {
await withMcpContext(async (response, context) => {
const disabledArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--no-javascript-evaluation'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const tool = navigatePage(disabledArgs);
await assert.rejects(
async () => {
await tool.handler(
{
params: {
url: 'javascript:alert(1)',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{
params: {
url: 'data:text/html,<div>test</div>',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Navigating to data: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{
params: {
url: 'vbscript:msgbox(1)',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Navigating to vbscript: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
});
});
it('throws when URL does not parse with new URL', async () => {
await withMcpContext(async (response, context) => {
const tool = navigatePage();
await assert.rejects(
async () => {
await tool.handler(
{
params: {
url: 'not a valid url',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Invalid URL: "not a valid url". URLs must be valid according to the URL standard.',
},
);
});
});
it('when dialog is open', async t => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
+82 -1
View File
@@ -9,6 +9,7 @@ import fs from 'node:fs';
import path from 'node:path';
import {describe, it} from 'node:test';
import {parseArguments} from '../../../src/config/mcp-options.js';
import {evaluate, navigate, screenshot} from '../../../src/tools/slim/tools.js';
import {screenshots} from '../../snapshot.js';
import {withMcpContext} from '../../utils.js';
@@ -48,7 +49,7 @@ describe('slim', () => {
it('navigates to correct page', async t => {
await withMcpContext(async (response, context) => {
await navigate.handler(
await navigate().handler(
{
params: {url: 'data:text/html,<div>Hello MCP</div>'},
page: context.getSelectedMcpPage(),
@@ -66,6 +67,86 @@ describe('slim', () => {
});
});
it('throws when URL does not parse with new URL', async () => {
await withMcpContext(async (response, context) => {
await assert.rejects(
async () => {
await navigate().handler(
{
params: {url: 'not a valid url'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Invalid URL: "not a valid url". URLs must be valid according to the URL standard.',
},
);
});
});
it('disallows javascript, data, and vbscript URLs when javascriptEvaluation is false', async () => {
await withMcpContext(async (response, context) => {
const disabledArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--slim', '--no-javascript-evaluation'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const tool = navigate(disabledArgs);
await assert.rejects(
async () => {
await tool.handler(
{
params: {url: 'javascript:alert(1)'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{
params: {url: 'data:text/html,<div>test</div>'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Navigating to data: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{
params: {url: 'vbscript:msgbox(1)'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Navigating to vbscript: URLs is not allowed when JavaScript evaluation is disabled.',
},
);
});
});
it('with default options', async () => {
await withMcpContext(async (response, context) => {
const fixture = screenshots.basic;
+81 -1
View File
@@ -7,7 +7,7 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';
import {isLocalhost} from '../../src/utils/url.js';
import {isLocalhost, validateUrl} from '../../src/utils/url.js';
describe('isLocalhost', () => {
it('should return true for valid localhost and loopback URLs', () => {
@@ -87,3 +87,83 @@ describe('isLocalhost', () => {
assert.strictEqual(isLocalhost('not a url'), false);
});
});
describe('validateUrl', () => {
it('should return URL object for valid URLs', () => {
assert.strictEqual(
validateUrl('https://example.com').href,
'https://example.com/',
);
assert.strictEqual(
validateUrl('http://localhost:3000').href,
'http://localhost:3000/',
);
assert.strictEqual(validateUrl('about:blank').href, 'about:blank');
assert.strictEqual(
validateUrl('data:text/html,<div>test</div>').href,
'data:text/html,<div>test</div>',
);
});
it('should reject URLs that do not parse with new URL', () => {
assert.throws(() => validateUrl('not a url'), /Invalid URL: "not a url"/);
assert.throws(() => validateUrl(''), /Invalid URL: ""/);
assert.throws(() => validateUrl('http://'), /Invalid URL: "http:\/\/"/);
assert.throws(() => validateUrl('://'), /Invalid URL: ":\/\/"/);
});
it('should allow javascript, data, and vbscript URLs when javascriptEvaluation is true or omitted', () => {
assert.strictEqual(
validateUrl('javascript:alert(1)').protocol,
'javascript:',
);
assert.strictEqual(
validateUrl('javascript:alert(1)', true).protocol,
'javascript:',
);
assert.strictEqual(
validateUrl('data:text/html,<div>test</div>').protocol,
'data:',
);
assert.strictEqual(
validateUrl('data:text/html,<div>test</div>', true).protocol,
'data:',
);
assert.strictEqual(validateUrl('vbscript:msgbox(1)').protocol, 'vbscript:');
assert.strictEqual(
validateUrl('vbscript:msgbox(1)', true).protocol,
'vbscript:',
);
});
it('should reject javascript, data, and vbscript URLs when javascriptEvaluation is false', () => {
assert.throws(
() => validateUrl('javascript:alert(1)', false),
/Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('JAVASCRIPT:alert(1)', false),
/Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('javascript:void(0)', false),
/Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('data:text/html,<div>test</div>', false),
/Navigating to data: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('DATA:text/html,<div>test</div>', false),
/Navigating to data: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('vbscript:msgbox(1)', false),
/Navigating to vbscript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('VBSCRIPT:msgbox(1)', false),
/Navigating to vbscript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
});
});