browserclaw 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1232,7 +1232,8 @@ async function assertBrowserNavigationAllowed(opts) {
1232
1232
  const hostname = parsed.hostname.toLowerCase();
1233
1233
  if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
1234
1234
  }
1235
- if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
1235
+ const ipOpts = { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange };
1236
+ if (await isInternalUrlResolved(rawUrl, opts.lookupFn, ipOpts)) {
1236
1237
  throw new InvalidBrowserNavigationUrlError(
1237
1238
  `Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1238
1239
  );
@@ -1347,7 +1348,7 @@ function isUnsupportedIPv4Literal(ip) {
1347
1348
  if (!parts.every(isStrictDecimalOctet)) return true;
1348
1349
  return false;
1349
1350
  }
1350
- function isInternalIP(ip) {
1351
+ function isInternalIP(ip, opts) {
1351
1352
  if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
1352
1353
  if (/^127\./.test(ip)) return true;
1353
1354
  if (/^10\./.test(ip)) return true;
@@ -1356,6 +1357,7 @@ function isInternalIP(ip) {
1356
1357
  if (/^169\.254\./.test(ip)) return true;
1357
1358
  if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
1358
1359
  if (ip === "0.0.0.0") return true;
1360
+ if (!opts?.allowRfc2544BenchmarkRange && /^198\.1[89]\./.test(ip)) return true;
1359
1361
  const lower = ip.toLowerCase();
1360
1362
  if (lower === "::1") return true;
1361
1363
  if (lower.startsWith("fe80:")) return true;
@@ -1364,11 +1366,11 @@ function isInternalIP(ip) {
1364
1366
  const embedded = extractEmbeddedIPv4(lower);
1365
1367
  if (embedded !== null) {
1366
1368
  if (embedded === "") return true;
1367
- return isInternalIP(embedded);
1369
+ return isInternalIP(embedded, opts);
1368
1370
  }
1369
1371
  return false;
1370
1372
  }
1371
- function isInternalUrl(url) {
1373
+ function isInternalUrl(url, opts) {
1372
1374
  let parsed;
1373
1375
  try {
1374
1376
  parsed = new URL(url);
@@ -1377,7 +1379,7 @@ function isInternalUrl(url) {
1377
1379
  }
1378
1380
  const hostname = parsed.hostname.toLowerCase();
1379
1381
  if (hostname === "localhost") return true;
1380
- if (isInternalIP(hostname)) return true;
1382
+ if (isInternalIP(hostname, opts)) return true;
1381
1383
  if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
1382
1384
  return true;
1383
1385
  }
@@ -1399,8 +1401,8 @@ async function assertSafeUploadPaths(paths) {
1399
1401
  }
1400
1402
  }
1401
1403
  }
1402
- async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1403
- if (isInternalUrl(url)) return true;
1404
+ async function isInternalUrlResolved(url, lookupFn = promises.lookup, opts) {
1405
+ if (isInternalUrl(url, opts)) return true;
1404
1406
  let parsed;
1405
1407
  try {
1406
1408
  parsed = new URL(url);
@@ -1409,12 +1411,25 @@ async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1409
1411
  }
1410
1412
  try {
1411
1413
  const { address } = await lookupFn(parsed.hostname);
1412
- if (isInternalIP(address)) return true;
1414
+ if (isInternalIP(address, opts)) return true;
1413
1415
  } catch {
1414
1416
  return true;
1415
1417
  }
1416
1418
  return false;
1417
1419
  }
1420
+ async function assertBrowserNavigationResultAllowed(opts) {
1421
+ const rawUrl = String(opts.url ?? "").trim();
1422
+ if (!rawUrl) return;
1423
+ let parsed;
1424
+ try {
1425
+ parsed = new URL(rawUrl);
1426
+ } catch {
1427
+ return;
1428
+ }
1429
+ if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || SAFE_NON_NETWORK_URLS.has(parsed.href)) {
1430
+ await assertBrowserNavigationAllowed(opts);
1431
+ }
1432
+ }
1418
1433
 
1419
1434
  // src/actions/interaction.ts
1420
1435
  async function clickViaPlaywright(opts) {
@@ -1620,7 +1635,9 @@ async function navigateViaPlaywright(opts) {
1620
1635
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1621
1636
  ensurePageState(page);
1622
1637
  await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
1623
- return { url: page.url() };
1638
+ const finalUrl = page.url();
1639
+ await assertBrowserNavigationResultAllowed({ url: finalUrl, ssrfPolicy: policy });
1640
+ return { url: finalUrl };
1624
1641
  }
1625
1642
  async function listPagesViaPlaywright(opts) {
1626
1643
  const { browser } = await connectBrowser(opts.cdpUrl);
@@ -1639,8 +1656,8 @@ async function listPagesViaPlaywright(opts) {
1639
1656
  }
1640
1657
  async function createPageViaPlaywright(opts) {
1641
1658
  const targetUrl = (opts.url ?? "").trim() || "about:blank";
1659
+ const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
1642
1660
  if (targetUrl !== "about:blank") {
1643
- const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
1644
1661
  await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
1645
1662
  }
1646
1663
  const { browser } = await connectBrowser(opts.cdpUrl);
@@ -1649,6 +1666,7 @@ async function createPageViaPlaywright(opts) {
1649
1666
  ensurePageState(page);
1650
1667
  if (targetUrl !== "about:blank") {
1651
1668
  await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
1669
+ await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
1652
1670
  }
1653
1671
  const tid = await pageTargetId(page).catch(() => null);
1654
1672
  if (!tid) throw new Error("Failed to get targetId for new page");