fix: filter out Chrome webui targets by default (#2648)

This commit is contained in:
Alex Rudenko
2026-09-03 13:38:15 +00:00
committed by GitHub
parent dad58bc749
commit 020c04890f
13 changed files with 760 additions and 75 deletions
+1
View File
@@ -18,3 +18,4 @@ This repository contains an MCP server and CLI for Chrome DevTools.
- Do not use `// @ts-nocheck` comments.
- Do not use `// @ts-expect-error` comments.
- Prefer `for..of` instead of `forEach`.
- Never type-check types that are already type safe (e.g. redundant `typeof` checks on statically typed variables).
+48 -26
View File
@@ -50,6 +50,7 @@ import type {TraceResult} from './processors/PerformanceTrace.js';
import type {Logger} from './types.js';
import type {ExtensionServiceWorker} from './types.js';
import {getTempFilePath, resolveCanonicalPath} from './utils/files.js';
import {isAllowedUrl} from './utils/url.js';
interface McpContextOptions {
// Whether the DevTools windows are exposed as pages for debugging of DevTools.
experimentalDevToolsDebugging: boolean;
@@ -70,6 +71,8 @@ interface McpContextOptions {
reconnected?: boolean;
// Custom navigation timeout in milliseconds to override default.
navigationTimeout?: number;
// Whether extension tools and targets are enabled.
categoryExtensions?: boolean;
}
// Page ids are handed out from a process-wide counter so they stay unique
@@ -164,6 +167,14 @@ export class McpContext implements Context {
#onTargetCreated = async (target: Target) => {
try {
const url = target.url();
if (
!isAllowedUrl(url, {
categoryExtensions: this.#options.categoryExtensions,
})
) {
return;
}
const page = await target.page();
if (!page) {
return;
@@ -604,36 +615,47 @@ export class McpContext implements Context {
const allPages = (
await this.browser.pages(this.#options.experimentalIncludeAllPages)
).filter(page => {
return (
this.#options.experimentalDevToolsDebugging ||
!page.url().startsWith('devtools://')
);
if (
!this.#options.experimentalDevToolsDebugging &&
page.url().startsWith('devtools://')
) {
return false;
}
return isAllowedUrl(page.url(), {
categoryExtensions: this.#options.categoryExtensions,
});
});
const allTargets = this.browser.targets();
const extensionTargets = allTargets.filter(target => {
return (
target.url().startsWith('chrome-extension://') &&
target.type() === 'page'
);
});
if (this.#options.categoryExtensions) {
const allTargets = this.browser.targets();
const extensionTargets = allTargets.filter(target => {
return (
target.url().startsWith('chrome-extension://') &&
target.type() === 'page'
);
});
await Promise.allSettled(
extensionTargets.map(async target => {
try {
let page = await target.page();
if (!page) {
page = await target.asPage();
await Promise.allSettled(
extensionTargets.map(async target => {
try {
let page = await target.page();
if (!page) {
page = await target.asPage();
}
this.#extensionPages.set(target, page);
if (
page &&
isAllowedUrl(page.url(), {categoryExtensions: true}) &&
!allPages.includes(page)
) {
allPages.push(page);
}
} catch (e) {
this.logger?.('Failed to get page for extension target', e);
}
this.#extensionPages.set(target, page);
if (page && !allPages.includes(page)) {
allPages.push(page);
}
} catch (e) {
this.logger?.('Failed to get page for extension target', e);
}
}),
);
}),
);
}
return allPages;
}
+6 -19
View File
@@ -13,34 +13,21 @@ import type {
Browser,
ChromeReleaseChannel,
LaunchOptions,
Target,
} from './third_party/index.js';
import {puppeteer} from './third_party/index.js';
import {logger, puppeteerLogger} from './utils/logger.js';
import {isAllowedUrl} from './utils/url.js';
let browser: Browser | undefined;
let browserMode: 'launched' | 'connected' | undefined;
function makeTargetFilter(enableExtensions = false) {
const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']);
if (!enableExtensions) {
ignoredPrefixes.add('chrome-extension://');
}
return function targetFilter(target: Target): boolean {
if (target.url() === 'chrome://newtab/') {
export function makeTargetFilter(enableExtensions = false) {
return function targetFilter(target: {url(): string}): boolean {
const url = target.url();
if (!url) {
return true;
}
// Could be the only page opened in the browser.
if (target.url().startsWith('chrome://inspect')) {
return true;
}
for (const prefix of ignoredPrefixes) {
if (target.url().startsWith(prefix)) {
return false;
}
}
return true;
return isAllowedUrl(url, {categoryExtensions: enableExtensions});
};
}
+1
View File
@@ -266,6 +266,7 @@ export class McpServer {
allowUnrestrictedPaths: this.#serverArgs.allowUnrestrictedPaths,
// Surfaces a one-time note in the next response after a reconnect.
reconnected: this.#context !== undefined,
categoryExtensions: this.#serverArgs.categoryExtensions,
});
this.#context.setRoots(this.#combinedRoots());
if (this.#lastClientRoots === undefined) {
+8 -2
View File
@@ -126,7 +126,10 @@ export const newPage = defineTool(args => {
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response, context) => {
validateUrl(request.params.url, args?.javascriptEvaluation);
validateUrl(request.params.url, {
javascriptEvaluation: args?.javascriptEvaluation,
categoryExtensions: args?.categoryExtensions,
});
const page = await context.newPage(
request.params.background,
@@ -203,7 +206,10 @@ export const navigatePage = definePageTool(args => {
}
if (request.params.url) {
validateUrl(request.params.url, args?.javascriptEvaluation);
validateUrl(request.params.url, {
javascriptEvaluation: args?.javascriptEvaluation,
categoryExtensions: args?.categoryExtensions,
});
}
let initScriptId: string | undefined;
+4 -1
View File
@@ -49,7 +49,10 @@ export const navigate = definePageTool(args => {
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response) => {
validateUrl(request.params.url, args?.javascriptEvaluation);
validateUrl(request.params.url, {
javascriptEvaluation: args?.javascriptEvaluation,
categoryExtensions: args?.categoryExtensions,
});
const page = request.page;
+77 -4
View File
@@ -49,17 +49,81 @@ export function isLocalhost(url?: string): boolean {
return false;
}
export interface ValidateUrlOptions {
javascriptEvaluation: boolean | undefined;
categoryExtensions: boolean | undefined;
}
export interface IsAllowedUrlOptions {
categoryExtensions: boolean | undefined;
}
/**
* Determines whether a URL is allowed for navigation and page tracking.
*
* Disallowed schemes:
* - chrome: (except chrome://newtab/ and chrome://inspect*)
* - chrome-untrusted:
* - chrome-extension: (unless categoryExtensions is true)
*
* @param url The URL string to test.
* @param options Configuration specifying whether extensions are enabled.
* @returns true if the URL is allowed.
*/
export function isAllowedUrl(
url: string,
options: IsAllowedUrlOptions,
): boolean {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return false;
}
if (parsed.protocol === 'chrome:') {
const host = parsed.hostname.toLowerCase();
const path = parsed.pathname.toLowerCase();
if (host === 'newtab' || host === 'new-tab-page' || host === 'inspect') {
return true;
}
if (
host === '' &&
(path === 'newtab' ||
path === 'new-tab-page' ||
path.startsWith('inspect'))
) {
return true;
}
}
if (
parsed.protocol === 'chrome:' ||
parsed.protocol === 'chrome-untrusted:'
) {
return false;
}
if (!options.categoryExtensions && parsed.protocol === 'chrome-extension:') {
return false;
}
return true;
}
const DISALLOWED_PROTOCOLS = new Set(['javascript:', 'data:', 'vbscript:']);
/**
* Validates a URL string by parsing it with `new URL` and checking for disallowed protocols.
* Validates a URL string by parsing it with `new URL` and checking for disallowed protocols and restricted schemes.
*
* @param url The URL string to validate.
* @param javascriptEvaluation Whether JavaScript evaluation is enabled.
* @param options Options object containing javascriptEvaluation and categoryExtensions.
* @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.
* @throws Error if the URL does not parse with `new URL`, or if JavaScript evaluation is disabled and a disallowed URL is passed,
* or if navigating to a restricted scheme.
*/
export function validateUrl(url: string, javascriptEvaluation?: boolean): URL {
export function validateUrl(url: string, options: ValidateUrlOptions): URL {
const {javascriptEvaluation, categoryExtensions} = options;
let parsed: URL;
try {
parsed = new URL(url);
@@ -78,5 +142,14 @@ export function validateUrl(url: string, javascriptEvaluation?: boolean): URL {
);
}
if (!isAllowedUrl(url, {categoryExtensions})) {
if (parsed.protocol === 'chrome-extension:') {
throw new Error(
`Navigating to chrome-extension: URLs is not allowed without --categoryExtensions.`,
);
}
throw new Error(`Navigating to ${parsed.protocol} URLs is not allowed.`);
}
return parsed;
}
+83
View File
@@ -134,6 +134,89 @@ describe('McpContext', () => {
},
);
});
it('drops pages reaching internal chrome or chrome-untrusted schemes by any route', async () => {
await withMcpContext(async (_response, context) => {
const page = await context.newPage();
const pageId = page.id;
assert.strictEqual(context.getPageById(pageId), page);
const urlStub = sinon
.stub(page.pptrPage, 'url')
.returns('chrome://settings');
try {
await context.createPagesSnapshot();
const listed = context.getPages();
assert.ok(
!listed.some(p => p.id === pageId),
'page reaching chrome://settings should be dropped from listing',
);
assert.throws(() => context.getPageById(pageId), /No page found/);
} finally {
urlStub.restore();
}
});
});
it('drops pages with chrome-extension schemes unless categoryExtensions is enabled', async () => {
await withMcpContext(async (_response, context) => {
const page = await context.newPage();
const pageId = page.id;
const urlStub = sinon
.stub(page.pptrPage, 'url')
.returns('chrome-extension://some-ext-id/popup.html');
try {
await context.createPagesSnapshot();
const listed = context.getPages();
assert.ok(
!listed.some(p => p.id === pageId),
'extension page should be dropped from listing when categoryExtensions is disabled',
);
assert.throws(() => context.getPageById(pageId), /No page found/);
} finally {
urlStub.restore();
}
});
});
it('keeps pages with chrome://newtab/ and chrome://inspect', async () => {
await withMcpContext(async (_response, context) => {
const page = await context.newPage();
const pageId = page.id;
const newtabStub = sinon
.stub(page.pptrPage, 'url')
.returns('chrome://newtab/');
try {
await context.createPagesSnapshot();
const listed = context.getPages();
assert.ok(
listed.some(p => p.id === pageId),
'chrome://newtab/ should be kept in listing',
);
assert.strictEqual(context.getPageById(pageId), page);
} finally {
newtabStub.restore();
}
const inspectStub = sinon
.stub(page.pptrPage, 'url')
.returns('chrome://inspect/#devices');
try {
await context.createPagesSnapshot();
const listed = context.getPages();
assert.ok(
listed.some(p => p.id === pageId),
'chrome://inspect should be kept in listing',
);
assert.strictEqual(context.getPageById(pageId), page);
} finally {
inspectStub.restore();
}
});
});
it('resolves uid from a non-selected page snapshot', async () => {
await withMcpContext(async (_response, context) => {
// Page 1: set content and snapshot
+69 -1
View File
@@ -11,7 +11,12 @@ import {describe, it} from 'node:test';
import {executablePath} from 'puppeteer';
import {detectDisplay, ensureBrowserConnected, launch} from '../src/browser.js';
import {
detectDisplay,
ensureBrowserConnected,
launch,
makeTargetFilter,
} from '../src/browser.js';
import type {Browser} from '../src/third_party/index.js';
import {serverHooks} from './server.js';
@@ -244,4 +249,67 @@ describe('browser', () => {
});
});
});
describe('makeTargetFilter', () => {
it('filters internal chrome and extension targets', () => {
const filterWithoutExtensions = makeTargetFilter(false);
const filterWithExtensions = makeTargetFilter(true);
const mockTarget = (url: string) => ({
url: () => url,
});
// Newtab and inspect allowances
assert.strictEqual(
filterWithoutExtensions(mockTarget('chrome://newtab/')),
true,
);
assert.strictEqual(
filterWithoutExtensions(mockTarget('chrome://inspect')),
true,
);
assert.strictEqual(
filterWithoutExtensions(mockTarget('chrome://inspect/#devices')),
true,
);
// Disallowed internal schemes
assert.strictEqual(
filterWithoutExtensions(mockTarget('chrome://settings')),
false,
);
assert.strictEqual(
filterWithoutExtensions(mockTarget('chrome://version')),
false,
);
assert.strictEqual(
filterWithoutExtensions(mockTarget('chrome-untrusted://terminal')),
false,
);
// Extensions toggle
assert.strictEqual(
filterWithoutExtensions(
mockTarget('chrome-extension://abcdef/popup.html'),
),
false,
);
assert.strictEqual(
filterWithExtensions(
mockTarget('chrome-extension://abcdef/popup.html'),
),
true,
);
// Web URLs
assert.strictEqual(
filterWithoutExtensions(mockTarget('https://example.com')),
true,
);
assert.strictEqual(
filterWithoutExtensions(mockTarget('about:blank')),
true,
);
});
});
});
+142
View File
@@ -319,6 +319,70 @@ describe('pages', () => {
);
});
});
it('rejects chrome: and chrome-untrusted: URLs', async () => {
await withMcpContext(async (response, context) => {
const tool = newPage();
await assert.rejects(
async () => {
await tool.handler(
{params: {url: 'chrome://settings'}},
response,
context,
);
},
{
message: 'Navigating to chrome: URLs is not allowed.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{params: {url: 'chrome-untrusted://terminal'}},
response,
context,
);
},
{
message: 'Navigating to chrome-untrusted: URLs is not allowed.',
},
);
assert.strictEqual(context.getPages().length, 1);
});
});
it('rejects chrome-extension: URLs unless categoryExtensions is enabled', async () => {
await withMcpContext(async (response, context) => {
const tool = newPage();
await assert.rejects(
async () => {
await tool.handler(
{params: {url: 'chrome-extension://abcdef/popup.html'}},
response,
context,
);
},
{
message:
'Navigating to chrome-extension: URLs is not allowed without --categoryExtensions.',
},
);
});
});
it('allows chrome://newtab/', async () => {
await withMcpContext(async (response, context) => {
const tool = newPage();
await tool.handler(
{params: {url: 'chrome://newtab/'}},
response,
context,
);
assert.ok(
context
.getSelectedMcpPage()
.pptrPage.url()
.startsWith('chrome://new'),
);
});
});
it('create a page in the background', async () => {
await withMcpContext(async (response, context) => {
const originalPage = context.getPageById(1);
@@ -1093,6 +1157,84 @@ describe('pages', () => {
});
});
it('rejects chrome: and chrome-untrusted: URLs', async () => {
await withMcpContext(async (response, context) => {
const tool = navigatePage();
await assert.rejects(
async () => {
await tool.handler(
{
params: {url: 'chrome://settings'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message: 'Navigating to chrome: URLs is not allowed.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{
params: {url: 'chrome-untrusted://terminal'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message: 'Navigating to chrome-untrusted: URLs is not allowed.',
},
);
});
});
it('rejects chrome-extension: URLs unless categoryExtensions is enabled', async () => {
await withMcpContext(async (response, context) => {
const tool = navigatePage();
await assert.rejects(
async () => {
await tool.handler(
{
params: {url: 'chrome-extension://abcdef/popup.html'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message:
'Navigating to chrome-extension: URLs is not allowed without --categoryExtensions.',
},
);
});
});
it('allows chrome://newtab/', async () => {
await withMcpContext(async (response, context) => {
const tool = navigatePage();
await tool.handler(
{
params: {url: 'chrome://newtab/'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.ok(
context
.getSelectedMcpPage()
.pptrPage.url()
.startsWith('chrome://new'),
);
});
});
it('when dialog is open', async t => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
+36
View File
@@ -147,6 +147,42 @@ describe('slim', () => {
});
});
it('rejects chrome: and chrome-untrusted: URLs', async () => {
await withMcpContext(async (response, context) => {
const tool = navigate();
await assert.rejects(
async () => {
await tool.handler(
{
params: {url: 'chrome://settings'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message: 'Navigating to chrome: URLs is not allowed.',
},
);
await assert.rejects(
async () => {
await tool.handler(
{
params: {url: 'chrome-untrusted://terminal'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{
message: 'Navigating to chrome-untrusted: URLs is not allowed.',
},
);
});
});
it('with default options', async () => {
await withMcpContext(async (response, context) => {
const fixture = screenshots.basic;
+1
View File
@@ -187,6 +187,7 @@ export async function withMcpContext(
navigationTimeout:
options.navigationTimeout ??
(process.platform === 'win32' ? 20000 : undefined),
categoryExtensions: args?.categoryExtensions,
},
Locator,
);
+284 -22
View File
@@ -7,7 +7,7 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';
import {isLocalhost, validateUrl} from '../../src/utils/url.js';
import {isAllowedUrl, isLocalhost, validateUrl} from '../../src/utils/url.js';
describe('isLocalhost', () => {
it('should return true for valid localhost and loopback URLs', () => {
@@ -88,82 +88,344 @@ describe('isLocalhost', () => {
});
});
const defaultOptions = {
javascriptEvaluation: undefined,
categoryExtensions: undefined,
};
describe('validateUrl', () => {
it('should return URL object for valid URLs', () => {
assert.strictEqual(
validateUrl('https://example.com').href,
validateUrl('https://example.com', defaultOptions).href,
'https://example.com/',
);
assert.strictEqual(
validateUrl('http://localhost:3000').href,
validateUrl('http://localhost:3000', defaultOptions).href,
'http://localhost:3000/',
);
assert.strictEqual(validateUrl('about:blank').href, 'about:blank');
assert.strictEqual(
validateUrl('data:text/html,<div>test</div>').href,
validateUrl('about:blank', defaultOptions).href,
'about:blank',
);
assert.strictEqual(
validateUrl('data:text/html,<div>test</div>', defaultOptions).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: ":\/\/"/);
assert.throws(
() => validateUrl('not a url', defaultOptions),
/Invalid URL: "not a url"/,
);
assert.throws(() => validateUrl('', defaultOptions), /Invalid URL: ""/);
assert.throws(
() => validateUrl('http://', defaultOptions),
/Invalid URL: "http:\/\/"/,
);
assert.throws(
() => validateUrl('://', defaultOptions),
/Invalid URL: ":\/\/"/,
);
});
it('should allow javascript, data, and vbscript URLs when javascriptEvaluation is true or omitted', () => {
assert.strictEqual(
validateUrl('javascript:alert(1)').protocol,
validateUrl('javascript:alert(1)', defaultOptions).protocol,
'javascript:',
);
assert.strictEqual(
validateUrl('javascript:alert(1)', true).protocol,
validateUrl('javascript:alert(1)', {
javascriptEvaluation: true,
categoryExtensions: undefined,
}).protocol,
'javascript:',
);
assert.strictEqual(
validateUrl('data:text/html,<div>test</div>').protocol,
validateUrl('data:text/html,<div>test</div>', defaultOptions).protocol,
'data:',
);
assert.strictEqual(
validateUrl('data:text/html,<div>test</div>', true).protocol,
validateUrl('data:text/html,<div>test</div>', {
javascriptEvaluation: true,
categoryExtensions: undefined,
}).protocol,
'data:',
);
assert.strictEqual(validateUrl('vbscript:msgbox(1)').protocol, 'vbscript:');
assert.strictEqual(
validateUrl('vbscript:msgbox(1)', true).protocol,
validateUrl('vbscript:msgbox(1)', defaultOptions).protocol,
'vbscript:',
);
assert.strictEqual(
validateUrl('vbscript:msgbox(1)', {
javascriptEvaluation: true,
categoryExtensions: undefined,
}).protocol,
'vbscript:',
);
});
it('should reject javascript, data, and vbscript URLs when javascriptEvaluation is false', () => {
assert.throws(
() => validateUrl('javascript:alert(1)', false),
() =>
validateUrl('javascript:alert(1)', {
javascriptEvaluation: false,
categoryExtensions: undefined,
}),
/Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('JAVASCRIPT:alert(1)', false),
() =>
validateUrl('JAVASCRIPT:alert(1)', {
javascriptEvaluation: false,
categoryExtensions: undefined,
}),
/Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('javascript:void(0)', false),
() =>
validateUrl('javascript:void(0)', {
javascriptEvaluation: false,
categoryExtensions: undefined,
}),
/Navigating to javascript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('data:text/html,<div>test</div>', false),
() =>
validateUrl('data:text/html,<div>test</div>', {
javascriptEvaluation: false,
categoryExtensions: undefined,
}),
/Navigating to data: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('DATA:text/html,<div>test</div>', false),
() =>
validateUrl('DATA:text/html,<div>test</div>', {
javascriptEvaluation: false,
categoryExtensions: undefined,
}),
/Navigating to data: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('vbscript:msgbox(1)', false),
() =>
validateUrl('vbscript:msgbox(1)', {
javascriptEvaluation: false,
categoryExtensions: undefined,
}),
/Navigating to vbscript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
assert.throws(
() => validateUrl('VBSCRIPT:msgbox(1)', false),
() =>
validateUrl('VBSCRIPT:msgbox(1)', {
javascriptEvaluation: false,
categoryExtensions: undefined,
}),
/Navigating to vbscript: URLs is not allowed when JavaScript evaluation is disabled\./,
);
});
it('should allow chrome://newtab/ and chrome://inspect', () => {
assert.strictEqual(
validateUrl('chrome://newtab/', defaultOptions).href,
'chrome://newtab/',
);
assert.strictEqual(
validateUrl('chrome://newtab', defaultOptions).href,
'chrome://newtab',
);
assert.strictEqual(
validateUrl('chrome://inspect', defaultOptions).href,
'chrome://inspect',
);
assert.strictEqual(
validateUrl('chrome://inspect/#devices', defaultOptions).href,
'chrome://inspect/#devices',
);
});
it('should reject chrome: and chrome-untrusted: URLs', () => {
assert.throws(
() => validateUrl('chrome://settings', defaultOptions),
/Navigating to chrome: URLs is not allowed\./,
);
assert.throws(
() => validateUrl('chrome://version', defaultOptions),
/Navigating to chrome: URLs is not allowed\./,
);
assert.throws(
() => validateUrl('chrome:version', defaultOptions),
/Navigating to chrome: URLs is not allowed\./,
);
assert.throws(
() => validateUrl('CHROME://version', defaultOptions),
/Navigating to chrome: URLs is not allowed\./,
);
assert.throws(
() => validateUrl('chrome-untrusted://terminal', defaultOptions),
/Navigating to chrome-untrusted: URLs is not allowed\./,
);
assert.throws(
() => validateUrl('chrome-untrusted:terminal', defaultOptions),
/Navigating to chrome-untrusted: URLs is not allowed\./,
);
});
it('should reject chrome-extension: URLs unless categoryExtensions is enabled', () => {
assert.throws(
() => validateUrl('chrome-extension://abcdef/popup.html', defaultOptions),
/Navigating to chrome-extension: URLs is not allowed without --categoryExtensions\./,
);
assert.throws(
() =>
validateUrl('chrome-extension://abcdef/popup.html', {
javascriptEvaluation: undefined,
categoryExtensions: false,
}),
/Navigating to chrome-extension: URLs is not allowed without --categoryExtensions\./,
);
assert.strictEqual(
validateUrl('chrome-extension://abcdef/popup.html', {
javascriptEvaluation: undefined,
categoryExtensions: true,
}).href,
'chrome-extension://abcdef/popup.html',
);
});
});
describe('isAllowedUrl', () => {
it('should allow chrome://newtab/ and chrome://inspect', () => {
assert.strictEqual(
isAllowedUrl('chrome://newtab/', {categoryExtensions: undefined}),
true,
);
assert.strictEqual(
isAllowedUrl('chrome://newtab', {categoryExtensions: undefined}),
true,
);
assert.strictEqual(
isAllowedUrl('CHROME://newtab/', {categoryExtensions: undefined}),
true,
);
assert.strictEqual(
isAllowedUrl('chrome://inspect', {categoryExtensions: undefined}),
true,
);
assert.strictEqual(
isAllowedUrl('chrome://inspect/', {categoryExtensions: undefined}),
true,
);
assert.strictEqual(
isAllowedUrl('chrome://inspect/#devices', {
categoryExtensions: undefined,
}),
true,
);
assert.strictEqual(
isAllowedUrl('CHROME://inspect', {categoryExtensions: undefined}),
true,
);
});
it('should disallow internal chrome: and chrome-untrusted: URLs', () => {
assert.strictEqual(
isAllowedUrl('chrome://settings', {categoryExtensions: undefined}),
false,
);
assert.strictEqual(
isAllowedUrl('chrome://version', {categoryExtensions: undefined}),
false,
);
assert.strictEqual(
isAllowedUrl('chrome:version', {categoryExtensions: undefined}),
false,
);
assert.strictEqual(
isAllowedUrl('CHROME://settings', {categoryExtensions: undefined}),
false,
);
assert.strictEqual(
isAllowedUrl('chrome-untrusted://terminal', {
categoryExtensions: undefined,
}),
false,
);
assert.strictEqual(
isAllowedUrl('chrome-untrusted:terminal', {
categoryExtensions: undefined,
}),
false,
);
assert.strictEqual(
isAllowedUrl('CHROME-UNTRUSTED://terminal', {
categoryExtensions: undefined,
}),
false,
);
});
it('should disallow chrome-extension: URLs when extensions are disabled and allow when enabled', () => {
assert.strictEqual(
isAllowedUrl('chrome-extension://abcdef/popup.html', {
categoryExtensions: undefined,
}),
false,
);
assert.strictEqual(
isAllowedUrl('chrome-extension://abcdef/popup.html', {
categoryExtensions: false,
}),
false,
);
assert.strictEqual(
isAllowedUrl('chrome-extension:abcdef', {categoryExtensions: false}),
false,
);
assert.strictEqual(
isAllowedUrl('chrome-extension://abcdef/popup.html', {
categoryExtensions: true,
}),
true,
);
assert.strictEqual(
isAllowedUrl('chrome-extension:abcdef', {categoryExtensions: true}),
true,
);
});
it('should allow standard web and navigation URLs', () => {
assert.strictEqual(
isAllowedUrl('https://example.com', {categoryExtensions: undefined}),
true,
);
assert.strictEqual(
isAllowedUrl('http://localhost:3000', {categoryExtensions: undefined}),
true,
);
assert.strictEqual(
isAllowedUrl('about:blank', {categoryExtensions: undefined}),
true,
);
assert.strictEqual(
isAllowedUrl('data:text/html,<div>test</div>', {
categoryExtensions: undefined,
}),
true,
);
});
it('should return false for unparseable URLs', () => {
assert.strictEqual(
isAllowedUrl('not a url', {categoryExtensions: undefined}),
false,
);
assert.strictEqual(
isAllowedUrl('', {categoryExtensions: undefined}),
false,
);
assert.strictEqual(
isAllowedUrl('://', {categoryExtensions: undefined}),
false,
);
});
});