MCPcopy
hub / github.com/garrytan/gstack / validateNavigationUrl

Function validateNavigationUrl

browse/src/url-validation.ts:228–298  ·  view source on GitHub ↗
(url: string)

Source from the content-addressed store, hash-verified

226 * - browser-manager.ts:restoreState
227 */
228export async function validateNavigationUrl(url: string): Promise<string> {
229 // Normalize non-standard file:// shapes before the URL parser sees them.
230 let normalized = url;
231 if (url.toLowerCase().startsWith('file:')) {
232 normalized = normalizeFileUrl(url);
233 }
234
235 let parsed: URL;
236 try {
237 parsed = new URL(normalized);
238 } catch {
239 throw new Error(`Invalid URL: ${url}`);
240 }
241
242 // file:// path: validate against safe-dirs and allow; otherwise defer to http(s) logic.
243 if (parsed.protocol === 'file:') {
244 // Reject non-empty non-localhost hosts (UNC / network paths).
245 if (parsed.host !== '' && parsed.host.toLowerCase() !== 'localhost') {
246 throw new Error(
247 `Unsupported file URL host: ${parsed.host}. Use file:///<absolute-path> for local files.`
248 );
249 }
250
251 // Convert URL → filesystem path with proper decoding (handles %20, %2F, etc.)
252 // fileURLToPath strips query + hash; we reattach them after validation so SPA
253 // fixture URLs like file:///tmp/app.html?route=home#login survive intact.
254 let fsPath: string;
255 try {
256 fsPath = fileURLToPath(parsed);
257 } catch (e: any) {
258 throw new Error(`Invalid file URL: ${url} (${e.message})`);
259 }
260
261 // Reject path traversal after decoding — e.g. file:///tmp/safe%2F..%2Fetc/passwd
262 // Note: fileURLToPath doesn't collapse .., so a literal '..' in the decoded path
263 // is suspicious. path.resolve will normalize it; check the result against safe dirs.
264 validateReadPath(fsPath);
265
266 // Return the canonical file:// URL derived from the filesystem path + original
267 // query + hash. This guarantees page.goto() gets a well-formed URL regardless
268 // of input shape while preserving SPA route/query params.
269 return pathToFileURL(fsPath).href + parsed.search + parsed.hash;
270 }
271
272 if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
273 throw new Error(
274 `Blocked: scheme "${parsed.protocol}" is not allowed. Only http:, https:, and file: URLs are permitted.`
275 );
276 }
277
278 const hostname = normalizeHostname(parsed.hostname.toLowerCase());
279
280 if (BLOCKED_METADATA_HOSTS.has(hostname) || isMetadataIp(hostname) || isBlockedIpv6(hostname)) {
281 throw new Error(
282 `Blocked: ${parsed.hostname} is a cloud metadata endpoint. Access is denied for security.`
283 );
284 }
285

Callers 6

handleWriteCommandFunction · 0.90
handleMetaCommandFunction · 0.90
newTabMethod · 0.90
restoreStateMethod · 0.90

Calls 6

validateReadPathFunction · 0.90
normalizeFileUrlFunction · 0.85
normalizeHostnameFunction · 0.85
isMetadataIpFunction · 0.85
isBlockedIpv6Function · 0.85
resolvesToBlockedIpFunction · 0.85

Tested by

no test coverage detected