openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
116 lines • 3.97 kB
JavaScript
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'ws:', 'wss:']);
const IPV4_LOOPBACK_REGEX = /^127(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
/**
* Determines whether a given URL string points to a localhost / loopback address.
*
* @param url The URL string to test.
* @returns true if the URL is a valid network URL pointing to localhost or a loopback IP.
*/
export function isLocalhost(url) {
if (!url || typeof url !== 'string' || url.trim() === '') {
return false;
}
let parsed;
try {
parsed = new URL(url);
}
catch {
return false;
}
if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
return false;
}
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, '');
// 1. Check localhost and RFC 6761 *.localhost subdomains
if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
return true;
}
// 2. Check IPv6 loopback ([::1] as returned by URL.hostname, or ::1)
if (hostname === '[::1]' || hostname === '::1') {
return true;
}
// 3. Check IPv4 loopback (127.0.0.0/8)
if (IPV4_LOOPBACK_REGEX.test(hostname)) {
return true;
}
return false;
}
/**
* 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, options) {
let parsed;
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 and restricted schemes.
*
* @param url The URL string to validate.
* @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,
* or if navigating to a restricted scheme.
*/
export function validateUrl(url, options) {
const { javascriptEvaluation, categoryExtensions } = options;
let parsed;
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.`);
}
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;
}
//# sourceMappingURL=url.js.map