diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index baad3ab5b4b6c..d9fad59b1ab11 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -29,7 +29,7 @@ import type { MCPProvider } from './sdk/proxyBackend'; export function decorateCommand(command: Command, version: string) { command - .option('--allowed-hosts ', 'comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to.', commaSeparatedList) + .option('--allowed-hosts ', 'comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass \'*\' to disable the host check.', commaSeparatedList) .option('--allowed-origins ', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList) .option('--blocked-origins ', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList) .option('--block-service-workers', 'block service workers') diff --git a/packages/playwright/src/mcp/sdk/http.ts b/packages/playwright/src/mcp/sdk/http.ts index 59fb59a8b1bb5..f39608740eb47 100644 --- a/packages/playwright/src/mcp/sdk/http.ts +++ b/packages/playwright/src/mcp/sdk/http.ts @@ -63,21 +63,24 @@ export async function installHttpTransport(httpServer: http.Server, serverBacken const url = httpAddressToString(httpServer.address()); const host = new URL(url).host; allowedHosts = (allowedHosts || [host]).map(h => h.toLowerCase()); + const allowAnyHost = allowedHosts.includes('*'); const sseSessions = new Map(); const streamableSessions = new Map(); httpServer.on('request', async (req, res) => { - const host = req.headers.host?.toLowerCase(); - if (!host) { - res.statusCode = 400; - return res.end('Missing host'); - } + if (!allowAnyHost) { + const host = req.headers.host?.toLowerCase(); + if (!host) { + res.statusCode = 400; + return res.end('Missing host'); + } - // Prevent DNS evil.com -> localhost rebind. - if (!allowedHosts.includes(host)) { - // Access from the browser is forbidden. - res.statusCode = 403; - return res.end('Access is only allowed at ' + allowedHosts.join(', ')); + // Prevent DNS evil.com -> localhost rebind. + if (!allowedHosts.includes(host)) { + // Access from the browser is forbidden. + res.statusCode = 403; + return res.end('Access is only allowed at ' + allowedHosts.join(', ')); + } } const url = new URL(`http://localhost${req.url}`); diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 01ef8ac4708b1..b5d2fcb34dda3 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -378,6 +378,14 @@ test('should respect allowed hosts (positive)', async ({ serverEndpoint }) => { expect(response.status).toBe(400); }); +test('should be able to allow any host', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint({ args: ['--allowed-hosts=*'] }); + const response = await fetch(url.href); + // 400 is expected for the mcp fetch. + expect(response.status).toBe(400); + expect(await response.text()).toBe('Invalid request'); +}); + async function findFreePort(): Promise { return new Promise((resolve, reject) => { const server = net.createServer();