browserclaw 0.3.6 → 0.3.7
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 +224 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +225 -182
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -7,6 +7,7 @@ var net = require('net');
|
|
|
7
7
|
var child_process = require('child_process');
|
|
8
8
|
var playwrightCore = require('playwright-core');
|
|
9
9
|
var promises = require('dns/promises');
|
|
10
|
+
var promises$1 = require('fs/promises');
|
|
10
11
|
|
|
11
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
13
|
|
|
@@ -1198,6 +1199,222 @@ function formatAriaNodes(nodes, limit) {
|
|
|
1198
1199
|
}
|
|
1199
1200
|
return out;
|
|
1200
1201
|
}
|
|
1202
|
+
var InvalidBrowserNavigationUrlError = class extends Error {
|
|
1203
|
+
constructor(message) {
|
|
1204
|
+
super(message);
|
|
1205
|
+
this.name = "InvalidBrowserNavigationUrlError";
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
1209
|
+
return { ssrfPolicy };
|
|
1210
|
+
}
|
|
1211
|
+
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1212
|
+
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
1213
|
+
async function assertBrowserNavigationAllowed(opts) {
|
|
1214
|
+
const rawUrl = String(opts.url ?? "").trim();
|
|
1215
|
+
let parsed;
|
|
1216
|
+
try {
|
|
1217
|
+
parsed = new URL(rawUrl);
|
|
1218
|
+
} catch {
|
|
1219
|
+
throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${rawUrl}"`);
|
|
1220
|
+
}
|
|
1221
|
+
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
|
|
1222
|
+
if (SAFE_NON_NETWORK_URLS.has(parsed.href)) return;
|
|
1223
|
+
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
|
|
1224
|
+
}
|
|
1225
|
+
const policy = opts.ssrfPolicy;
|
|
1226
|
+
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1227
|
+
const allowedHostnames = [
|
|
1228
|
+
...policy?.allowedHostnames ?? [],
|
|
1229
|
+
...policy?.hostnameAllowlist ?? []
|
|
1230
|
+
];
|
|
1231
|
+
if (allowedHostnames.length) {
|
|
1232
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1233
|
+
if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
|
|
1234
|
+
}
|
|
1235
|
+
if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
|
|
1236
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1237
|
+
`Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async function assertSafeOutputPath(path2, allowedRoots) {
|
|
1242
|
+
if (!path2 || typeof path2 !== "string") {
|
|
1243
|
+
throw new Error("Output path is required.");
|
|
1244
|
+
}
|
|
1245
|
+
const normalized = path.normalize(path2);
|
|
1246
|
+
if (normalized.includes("..")) {
|
|
1247
|
+
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
1248
|
+
}
|
|
1249
|
+
if (allowedRoots?.length) {
|
|
1250
|
+
const resolved = path.resolve(normalized);
|
|
1251
|
+
let parentReal;
|
|
1252
|
+
try {
|
|
1253
|
+
parentReal = await promises$1.realpath(path.dirname(resolved));
|
|
1254
|
+
} catch {
|
|
1255
|
+
throw new Error(`Unsafe output path: parent directory is inaccessible for "${path2}".`);
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
const targetStat = await promises$1.lstat(resolved);
|
|
1259
|
+
if (targetStat.isSymbolicLink()) {
|
|
1260
|
+
throw new Error(`Unsafe output path: "${path2}" is a symbolic link.`);
|
|
1261
|
+
}
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
if (e.code !== "ENOENT") throw e;
|
|
1264
|
+
}
|
|
1265
|
+
const results = await Promise.all(
|
|
1266
|
+
allowedRoots.map(async (root) => {
|
|
1267
|
+
try {
|
|
1268
|
+
const rootStat = await promises$1.lstat(path.resolve(root));
|
|
1269
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return false;
|
|
1270
|
+
const rootReal = await promises$1.realpath(path.resolve(root));
|
|
1271
|
+
return parentReal === rootReal || parentReal.startsWith(rootReal + path.sep);
|
|
1272
|
+
} catch {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
})
|
|
1276
|
+
);
|
|
1277
|
+
if (!results.some(Boolean)) {
|
|
1278
|
+
throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function expandIPv6(ip) {
|
|
1283
|
+
let normalized = ip;
|
|
1284
|
+
const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1285
|
+
if (v4Match) {
|
|
1286
|
+
const octets = v4Match[2].split(".").map(Number);
|
|
1287
|
+
if (octets.some((o) => o > 255)) return null;
|
|
1288
|
+
const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
|
|
1289
|
+
const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
|
|
1290
|
+
normalized = v4Match[1] + hexHi + ":" + hexLo;
|
|
1291
|
+
}
|
|
1292
|
+
const halves = normalized.split("::");
|
|
1293
|
+
if (halves.length > 2) return null;
|
|
1294
|
+
if (halves.length === 2) {
|
|
1295
|
+
const left = halves[0] !== "" ? halves[0].split(":") : [];
|
|
1296
|
+
const right = halves[1] !== "" ? halves[1].split(":") : [];
|
|
1297
|
+
const needed = 8 - left.length - right.length;
|
|
1298
|
+
if (needed < 0) return null;
|
|
1299
|
+
const groups2 = [...left, ...Array(needed).fill("0"), ...right];
|
|
1300
|
+
if (groups2.length !== 8) return null;
|
|
1301
|
+
return groups2.map((g) => g.padStart(4, "0")).join(":");
|
|
1302
|
+
}
|
|
1303
|
+
const groups = normalized.split(":");
|
|
1304
|
+
if (groups.length !== 8) return null;
|
|
1305
|
+
return groups.map((g) => g.padStart(4, "0")).join(":");
|
|
1306
|
+
}
|
|
1307
|
+
function hexToIPv4(hiHex, loHex) {
|
|
1308
|
+
const hi = parseInt(hiHex, 16);
|
|
1309
|
+
const lo = parseInt(loHex, 16);
|
|
1310
|
+
return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
1311
|
+
}
|
|
1312
|
+
function extractEmbeddedIPv4(lower) {
|
|
1313
|
+
if (lower.startsWith("::ffff:")) {
|
|
1314
|
+
return lower.slice(7);
|
|
1315
|
+
}
|
|
1316
|
+
const expanded = expandIPv6(lower);
|
|
1317
|
+
if (expanded === null) return "";
|
|
1318
|
+
const groups = expanded.split(":");
|
|
1319
|
+
if (groups.length !== 8) return "";
|
|
1320
|
+
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
|
|
1321
|
+
return hexToIPv4(groups[6], groups[7]);
|
|
1322
|
+
}
|
|
1323
|
+
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
|
|
1324
|
+
return hexToIPv4(groups[6], groups[7]);
|
|
1325
|
+
}
|
|
1326
|
+
if (groups[0] === "2002") {
|
|
1327
|
+
return hexToIPv4(groups[1], groups[2]);
|
|
1328
|
+
}
|
|
1329
|
+
if (groups[0] === "2001" && groups[1] === "0000") {
|
|
1330
|
+
const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1331
|
+
const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1332
|
+
return hexToIPv4(hiXored, loXored);
|
|
1333
|
+
}
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
function isStrictDecimalOctet(part) {
|
|
1337
|
+
if (!/^[0-9]+$/.test(part)) return false;
|
|
1338
|
+
const n = parseInt(part, 10);
|
|
1339
|
+
if (n < 0 || n > 255) return false;
|
|
1340
|
+
if (String(n) !== part) return false;
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
function isUnsupportedIPv4Literal(ip) {
|
|
1344
|
+
if (/^[0-9]+$/.test(ip)) return true;
|
|
1345
|
+
const parts = ip.split(".");
|
|
1346
|
+
if (parts.length !== 4) return true;
|
|
1347
|
+
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
function isInternalIP(ip) {
|
|
1351
|
+
if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
|
|
1352
|
+
if (/^127\./.test(ip)) return true;
|
|
1353
|
+
if (/^10\./.test(ip)) return true;
|
|
1354
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1355
|
+
if (/^192\.168\./.test(ip)) return true;
|
|
1356
|
+
if (/^169\.254\./.test(ip)) return true;
|
|
1357
|
+
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1358
|
+
if (ip === "0.0.0.0") return true;
|
|
1359
|
+
const lower = ip.toLowerCase();
|
|
1360
|
+
if (lower === "::1") return true;
|
|
1361
|
+
if (lower.startsWith("fe80:")) return true;
|
|
1362
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1363
|
+
if (lower.startsWith("ff")) return true;
|
|
1364
|
+
const embedded = extractEmbeddedIPv4(lower);
|
|
1365
|
+
if (embedded !== null) {
|
|
1366
|
+
if (embedded === "") return true;
|
|
1367
|
+
return isInternalIP(embedded);
|
|
1368
|
+
}
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
function isInternalUrl(url) {
|
|
1372
|
+
let parsed;
|
|
1373
|
+
try {
|
|
1374
|
+
parsed = new URL(url);
|
|
1375
|
+
} catch {
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1379
|
+
if (hostname === "localhost") return true;
|
|
1380
|
+
if (isInternalIP(hostname)) return true;
|
|
1381
|
+
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
async function assertSafeUploadPaths(paths) {
|
|
1387
|
+
for (const filePath of paths) {
|
|
1388
|
+
let stat;
|
|
1389
|
+
try {
|
|
1390
|
+
stat = await promises$1.lstat(filePath);
|
|
1391
|
+
} catch {
|
|
1392
|
+
throw new Error(`Upload path does not exist or is inaccessible: "${filePath}".`);
|
|
1393
|
+
}
|
|
1394
|
+
if (stat.isSymbolicLink()) {
|
|
1395
|
+
throw new Error(`Upload path is a symbolic link: "${filePath}".`);
|
|
1396
|
+
}
|
|
1397
|
+
if (!stat.isFile()) {
|
|
1398
|
+
throw new Error(`Upload path is not a regular file: "${filePath}".`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
|
|
1403
|
+
if (isInternalUrl(url)) return true;
|
|
1404
|
+
let parsed;
|
|
1405
|
+
try {
|
|
1406
|
+
parsed = new URL(url);
|
|
1407
|
+
} catch {
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
const { address } = await lookupFn(parsed.hostname);
|
|
1412
|
+
if (isInternalIP(address)) return true;
|
|
1413
|
+
} catch {
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1201
1418
|
|
|
1202
1419
|
// src/actions/interaction.ts
|
|
1203
1420
|
async function clickViaPlaywright(opts) {
|
|
@@ -1330,6 +1547,7 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
1330
1547
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1331
1548
|
const locator = opts.ref ? refLocator(page, opts.ref) : opts.element ? page.locator(opts.element).first() : null;
|
|
1332
1549
|
if (!locator) throw new Error("Either ref or element is required for setInputFiles");
|
|
1550
|
+
await assertSafeUploadPaths(opts.paths);
|
|
1333
1551
|
try {
|
|
1334
1552
|
await locator.setInputFiles(opts.paths);
|
|
1335
1553
|
} catch (err) {
|
|
@@ -1373,7 +1591,9 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
1373
1591
|
const handler = async (fc) => {
|
|
1374
1592
|
clearTimeout(timer);
|
|
1375
1593
|
try {
|
|
1376
|
-
|
|
1594
|
+
const paths = opts.paths ?? [];
|
|
1595
|
+
if (paths.length > 0) await assertSafeUploadPaths(paths);
|
|
1596
|
+
await fc.setFiles(paths);
|
|
1377
1597
|
resolve2();
|
|
1378
1598
|
} catch (err) {
|
|
1379
1599
|
reject(err);
|
|
@@ -1391,183 +1611,6 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
1391
1611
|
ensurePageState(page);
|
|
1392
1612
|
await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
|
|
1393
1613
|
}
|
|
1394
|
-
var InvalidBrowserNavigationUrlError = class extends Error {
|
|
1395
|
-
constructor(message) {
|
|
1396
|
-
super(message);
|
|
1397
|
-
this.name = "InvalidBrowserNavigationUrlError";
|
|
1398
|
-
}
|
|
1399
|
-
};
|
|
1400
|
-
function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
1401
|
-
return { ssrfPolicy };
|
|
1402
|
-
}
|
|
1403
|
-
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1404
|
-
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
1405
|
-
async function assertBrowserNavigationAllowed(opts) {
|
|
1406
|
-
const rawUrl = String(opts.url ?? "").trim();
|
|
1407
|
-
let parsed;
|
|
1408
|
-
try {
|
|
1409
|
-
parsed = new URL(rawUrl);
|
|
1410
|
-
} catch {
|
|
1411
|
-
throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${rawUrl}"`);
|
|
1412
|
-
}
|
|
1413
|
-
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
|
|
1414
|
-
if (SAFE_NON_NETWORK_URLS.has(parsed.href)) return;
|
|
1415
|
-
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
|
|
1416
|
-
}
|
|
1417
|
-
const policy = opts.ssrfPolicy;
|
|
1418
|
-
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1419
|
-
const allowedHostnames = [
|
|
1420
|
-
...policy?.allowedHostnames ?? [],
|
|
1421
|
-
...policy?.hostnameAllowlist ?? []
|
|
1422
|
-
];
|
|
1423
|
-
if (allowedHostnames.length) {
|
|
1424
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1425
|
-
if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
|
|
1426
|
-
}
|
|
1427
|
-
if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
|
|
1428
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
1429
|
-
`Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1430
|
-
);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
function assertSafeOutputPath(path2, allowedRoots) {
|
|
1434
|
-
if (!path2 || typeof path2 !== "string") {
|
|
1435
|
-
throw new Error("Output path is required.");
|
|
1436
|
-
}
|
|
1437
|
-
const normalized = path.normalize(path2);
|
|
1438
|
-
if (normalized.includes("..")) {
|
|
1439
|
-
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
1440
|
-
}
|
|
1441
|
-
if (allowedRoots?.length) {
|
|
1442
|
-
const resolved = path.resolve(normalized);
|
|
1443
|
-
const withinRoot = allowedRoots.some((root) => {
|
|
1444
|
-
const normalizedRoot = path.resolve(root);
|
|
1445
|
-
return resolved === normalizedRoot || resolved.startsWith(normalizedRoot + path.sep);
|
|
1446
|
-
});
|
|
1447
|
-
if (!withinRoot) {
|
|
1448
|
-
throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
function expandIPv6(ip) {
|
|
1453
|
-
let normalized = ip;
|
|
1454
|
-
const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1455
|
-
if (v4Match) {
|
|
1456
|
-
const octets = v4Match[2].split(".").map(Number);
|
|
1457
|
-
if (octets.some((o) => o > 255)) return null;
|
|
1458
|
-
const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
|
|
1459
|
-
const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
|
|
1460
|
-
normalized = v4Match[1] + hexHi + ":" + hexLo;
|
|
1461
|
-
}
|
|
1462
|
-
const halves = normalized.split("::");
|
|
1463
|
-
if (halves.length > 2) return null;
|
|
1464
|
-
if (halves.length === 2) {
|
|
1465
|
-
const left = halves[0] !== "" ? halves[0].split(":") : [];
|
|
1466
|
-
const right = halves[1] !== "" ? halves[1].split(":") : [];
|
|
1467
|
-
const needed = 8 - left.length - right.length;
|
|
1468
|
-
if (needed < 0) return null;
|
|
1469
|
-
const groups2 = [...left, ...Array(needed).fill("0"), ...right];
|
|
1470
|
-
if (groups2.length !== 8) return null;
|
|
1471
|
-
return groups2.map((g) => g.padStart(4, "0")).join(":");
|
|
1472
|
-
}
|
|
1473
|
-
const groups = normalized.split(":");
|
|
1474
|
-
if (groups.length !== 8) return null;
|
|
1475
|
-
return groups.map((g) => g.padStart(4, "0")).join(":");
|
|
1476
|
-
}
|
|
1477
|
-
function hexToIPv4(hiHex, loHex) {
|
|
1478
|
-
const hi = parseInt(hiHex, 16);
|
|
1479
|
-
const lo = parseInt(loHex, 16);
|
|
1480
|
-
return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
1481
|
-
}
|
|
1482
|
-
function extractEmbeddedIPv4(lower) {
|
|
1483
|
-
if (lower.startsWith("::ffff:")) {
|
|
1484
|
-
return lower.slice(7);
|
|
1485
|
-
}
|
|
1486
|
-
const expanded = expandIPv6(lower);
|
|
1487
|
-
if (expanded === null) return "";
|
|
1488
|
-
const groups = expanded.split(":");
|
|
1489
|
-
if (groups.length !== 8) return "";
|
|
1490
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
|
|
1491
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1492
|
-
}
|
|
1493
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
|
|
1494
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1495
|
-
}
|
|
1496
|
-
if (groups[0] === "2002") {
|
|
1497
|
-
return hexToIPv4(groups[1], groups[2]);
|
|
1498
|
-
}
|
|
1499
|
-
if (groups[0] === "2001" && groups[1] === "0000") {
|
|
1500
|
-
const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1501
|
-
const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1502
|
-
return hexToIPv4(hiXored, loXored);
|
|
1503
|
-
}
|
|
1504
|
-
return null;
|
|
1505
|
-
}
|
|
1506
|
-
function isStrictDecimalOctet(part) {
|
|
1507
|
-
if (!/^[0-9]+$/.test(part)) return false;
|
|
1508
|
-
const n = parseInt(part, 10);
|
|
1509
|
-
if (n < 0 || n > 255) return false;
|
|
1510
|
-
if (String(n) !== part) return false;
|
|
1511
|
-
return true;
|
|
1512
|
-
}
|
|
1513
|
-
function isUnsupportedIPv4Literal(ip) {
|
|
1514
|
-
if (/^[0-9]+$/.test(ip)) return true;
|
|
1515
|
-
const parts = ip.split(".");
|
|
1516
|
-
if (parts.length !== 4) return true;
|
|
1517
|
-
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1518
|
-
return false;
|
|
1519
|
-
}
|
|
1520
|
-
function isInternalIP(ip) {
|
|
1521
|
-
if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
|
|
1522
|
-
if (/^127\./.test(ip)) return true;
|
|
1523
|
-
if (/^10\./.test(ip)) return true;
|
|
1524
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1525
|
-
if (/^192\.168\./.test(ip)) return true;
|
|
1526
|
-
if (/^169\.254\./.test(ip)) return true;
|
|
1527
|
-
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1528
|
-
if (ip === "0.0.0.0") return true;
|
|
1529
|
-
const lower = ip.toLowerCase();
|
|
1530
|
-
if (lower === "::1") return true;
|
|
1531
|
-
if (lower.startsWith("fe80:")) return true;
|
|
1532
|
-
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1533
|
-
const embedded = extractEmbeddedIPv4(lower);
|
|
1534
|
-
if (embedded !== null) {
|
|
1535
|
-
if (embedded === "") return true;
|
|
1536
|
-
return isInternalIP(embedded);
|
|
1537
|
-
}
|
|
1538
|
-
return false;
|
|
1539
|
-
}
|
|
1540
|
-
function isInternalUrl(url) {
|
|
1541
|
-
let parsed;
|
|
1542
|
-
try {
|
|
1543
|
-
parsed = new URL(url);
|
|
1544
|
-
} catch {
|
|
1545
|
-
return true;
|
|
1546
|
-
}
|
|
1547
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1548
|
-
if (hostname === "localhost") return true;
|
|
1549
|
-
if (isInternalIP(hostname)) return true;
|
|
1550
|
-
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1551
|
-
return true;
|
|
1552
|
-
}
|
|
1553
|
-
return false;
|
|
1554
|
-
}
|
|
1555
|
-
async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
|
|
1556
|
-
if (isInternalUrl(url)) return true;
|
|
1557
|
-
let parsed;
|
|
1558
|
-
try {
|
|
1559
|
-
parsed = new URL(url);
|
|
1560
|
-
} catch {
|
|
1561
|
-
return true;
|
|
1562
|
-
}
|
|
1563
|
-
try {
|
|
1564
|
-
const { address } = await lookupFn(parsed.hostname);
|
|
1565
|
-
if (isInternalIP(address)) return true;
|
|
1566
|
-
} catch {
|
|
1567
|
-
return true;
|
|
1568
|
-
}
|
|
1569
|
-
return false;
|
|
1570
|
-
}
|
|
1571
1614
|
|
|
1572
1615
|
// src/actions/navigation.ts
|
|
1573
1616
|
async function navigateViaPlaywright(opts) {
|
|
@@ -1759,7 +1802,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
1759
1802
|
|
|
1760
1803
|
// src/actions/download.ts
|
|
1761
1804
|
async function downloadViaPlaywright(opts) {
|
|
1762
|
-
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1805
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1763
1806
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1764
1807
|
ensurePageState(page);
|
|
1765
1808
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
@@ -1786,7 +1829,7 @@ async function waitForDownloadViaPlaywright(opts) {
|
|
|
1786
1829
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1787
1830
|
const download = await page.waitForEvent("download", { timeout });
|
|
1788
1831
|
const savePath = opts.path ?? download.suggestedFilename();
|
|
1789
|
-
assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
1832
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
1790
1833
|
await download.saveAs(savePath);
|
|
1791
1834
|
return {
|
|
1792
1835
|
url: download.url(),
|
|
@@ -1970,7 +2013,7 @@ async function traceStartViaPlaywright(opts) {
|
|
|
1970
2013
|
});
|
|
1971
2014
|
}
|
|
1972
2015
|
async function traceStopViaPlaywright(opts) {
|
|
1973
|
-
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
2016
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1974
2017
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1975
2018
|
ensurePageState(page);
|
|
1976
2019
|
const context = page.context();
|