@kaitranntt/ccs 7.78.0-dev.5 → 7.78.0-dev.6
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/lib/mcp/ccs-browser-server.cjs +153 -6
- package/package.json +1 -1
|
@@ -151,6 +151,8 @@ const DEFAULT_DRAG_STEPS = 5;
|
|
|
151
151
|
const MAX_POINTER_ACTIONS = 25;
|
|
152
152
|
const SESSION_START_SETTLE_WINDOW_MS = 250;
|
|
153
153
|
const MAX_ARTIFACT_FILE_BYTES = 5 * 1024 * 1024;
|
|
154
|
+
const MAX_LOCAL_TRANSFER_FILE_BYTES = 10 * 1024 * 1024;
|
|
155
|
+
const MAX_LOCAL_TRANSFER_FILES = 10;
|
|
154
156
|
const SAFE_ARTIFACT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
155
157
|
const SESSION_CANCELED_ERROR_CODE = 'SESSION_CANCELED';
|
|
156
158
|
|
|
@@ -177,6 +179,37 @@ const recentDownloads = [];
|
|
|
177
179
|
const interceptSessionsByPageId = new Map();
|
|
178
180
|
let browserDownloadSession = null;
|
|
179
181
|
let sessionDownloadDir = '';
|
|
182
|
+
const SENSITIVE_LOCAL_PATH_SEGMENTS = new Set([
|
|
183
|
+
'.ssh',
|
|
184
|
+
'.gnupg',
|
|
185
|
+
'.aws',
|
|
186
|
+
'.azure',
|
|
187
|
+
'.kube',
|
|
188
|
+
'.docker',
|
|
189
|
+
'.npmrc',
|
|
190
|
+
'.netrc',
|
|
191
|
+
'.pypirc',
|
|
192
|
+
'.config',
|
|
193
|
+
'.claude',
|
|
194
|
+
'.ccs',
|
|
195
|
+
]);
|
|
196
|
+
const SENSITIVE_LOCAL_FILE_NAMES = new Set([
|
|
197
|
+
'.env',
|
|
198
|
+
'id_rsa',
|
|
199
|
+
'id_dsa',
|
|
200
|
+
'id_ecdsa',
|
|
201
|
+
'id_ed25519',
|
|
202
|
+
'known_hosts',
|
|
203
|
+
'authorized_keys',
|
|
204
|
+
'credentials',
|
|
205
|
+
'credentials.json',
|
|
206
|
+
'config.json',
|
|
207
|
+
'settings.json',
|
|
208
|
+
'history',
|
|
209
|
+
'.bash_history',
|
|
210
|
+
'.zsh_history',
|
|
211
|
+
'.fish_history',
|
|
212
|
+
]);
|
|
180
213
|
const MAX_RECENT_REQUESTS = 100;
|
|
181
214
|
const MAX_RECENT_DOWNLOADS = 100;
|
|
182
215
|
const FETCH_FAIL_ERROR_REASON = 'Failed';
|
|
@@ -1438,10 +1471,105 @@ function getSessionDownloadPath() {
|
|
|
1438
1471
|
return sessionDownloadDir;
|
|
1439
1472
|
}
|
|
1440
1473
|
|
|
1474
|
+
function splitConfiguredPathRoots(value) {
|
|
1475
|
+
return String(value || '')
|
|
1476
|
+
.split(path.delimiter)
|
|
1477
|
+
.map((entry) => entry.trim())
|
|
1478
|
+
.filter(Boolean)
|
|
1479
|
+
.map((entry) => path.resolve(entry));
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function getDownloadSafeRoots() {
|
|
1483
|
+
return [
|
|
1484
|
+
getSessionDownloadPath(),
|
|
1485
|
+
...splitConfiguredPathRoots(process.env.CCS_BROWSER_DOWNLOAD_ROOTS),
|
|
1486
|
+
];
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function getUploadSafeRoots() {
|
|
1490
|
+
return [
|
|
1491
|
+
getSessionDownloadPath(),
|
|
1492
|
+
...splitConfiguredPathRoots(process.env.CCS_BROWSER_UPLOAD_ROOTS),
|
|
1493
|
+
];
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function getNearestExistingAncestor(candidatePath) {
|
|
1497
|
+
let currentPath = candidatePath;
|
|
1498
|
+
while (!fs.existsSync(currentPath)) {
|
|
1499
|
+
const parentPath = path.dirname(currentPath);
|
|
1500
|
+
if (parentPath === currentPath) {
|
|
1501
|
+
return currentPath;
|
|
1502
|
+
}
|
|
1503
|
+
currentPath = parentPath;
|
|
1504
|
+
}
|
|
1505
|
+
return currentPath;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function resolveExistingRoot(rootPath) {
|
|
1509
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
1510
|
+
return fs.realpathSync(rootPath);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function resolvePathWithRealAncestor(candidatePath) {
|
|
1514
|
+
const resolvedPath = path.resolve(candidatePath);
|
|
1515
|
+
const ancestorPath = getNearestExistingAncestor(resolvedPath);
|
|
1516
|
+
const realAncestorPath = fs.realpathSync(ancestorPath);
|
|
1517
|
+
const relativeSuffix = path.relative(ancestorPath, resolvedPath);
|
|
1518
|
+
return relativeSuffix ? path.resolve(realAncestorPath, relativeSuffix) : realAncestorPath;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function isPathInsideRoot(candidatePath, rootPath) {
|
|
1522
|
+
const relativePath = path.relative(rootPath, candidatePath);
|
|
1523
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function findContainingRoot(candidatePath, rootPaths) {
|
|
1527
|
+
return rootPaths.find((rootPath) => isPathInsideRoot(candidatePath, rootPath)) || '';
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function getLocalPathSegments(candidatePath, rootPath) {
|
|
1531
|
+
const rootSegments = path.resolve(rootPath).split(path.sep).filter(Boolean);
|
|
1532
|
+
const relativePath = path.relative(rootPath, candidatePath);
|
|
1533
|
+
const relativeSegments = relativePath.split(path.sep).filter(Boolean);
|
|
1534
|
+
return [...rootSegments, ...relativeSegments];
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function assertNoSensitiveLocalPathSegments(candidatePath, rootPath, label) {
|
|
1538
|
+
const segments = getLocalPathSegments(candidatePath, rootPath);
|
|
1539
|
+
for (const segment of segments) {
|
|
1540
|
+
const normalizedSegment = segment.toLowerCase();
|
|
1541
|
+
if (normalizedSegment.startsWith('.') || SENSITIVE_LOCAL_PATH_SEGMENTS.has(normalizedSegment)) {
|
|
1542
|
+
throw new Error(`${label} cannot include hidden or sensitive path segment: ${segment}`);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const fileName = path.basename(candidatePath).toLowerCase();
|
|
1547
|
+
if (SENSITIVE_LOCAL_FILE_NAMES.has(fileName)) {
|
|
1548
|
+
throw new Error(
|
|
1549
|
+
`${label} cannot reference sensitive file name: ${path.basename(candidatePath)}`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1441
1554
|
function ensureWritableDirectory(downloadPath) {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1555
|
+
const resolvedPath = path.resolve(downloadPath);
|
|
1556
|
+
const candidatePath = resolvePathWithRealAncestor(resolvedPath);
|
|
1557
|
+
const safeRoots = getDownloadSafeRoots().map(resolveExistingRoot);
|
|
1558
|
+
const containingRoot = findContainingRoot(candidatePath, safeRoots);
|
|
1559
|
+
if (!containingRoot) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
'downloadPath must be inside the browser session download directory or a CCS_BROWSER_DOWNLOAD_ROOTS entry'
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
assertNoSensitiveLocalPathSegments(candidatePath, containingRoot, 'downloadPath');
|
|
1565
|
+
|
|
1566
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
1567
|
+
const realDownloadPath = fs.realpathSync(resolvedPath);
|
|
1568
|
+
if (!isPathInsideRoot(realDownloadPath, containingRoot)) {
|
|
1569
|
+
throw new Error('downloadPath cannot traverse outside the allowed download root');
|
|
1570
|
+
}
|
|
1571
|
+
fs.accessSync(realDownloadPath, fs.constants.W_OK);
|
|
1572
|
+
return realDownloadPath;
|
|
1445
1573
|
}
|
|
1446
1574
|
|
|
1447
1575
|
function pushRecentDownload(entry) {
|
|
@@ -2418,16 +2546,35 @@ function buildFileInputHandleExpression(selector, nth, frameSelector, pierceShad
|
|
|
2418
2546
|
}
|
|
2419
2547
|
|
|
2420
2548
|
function validateLocalFiles(files) {
|
|
2549
|
+
if (files.length > MAX_LOCAL_TRANSFER_FILES) {
|
|
2550
|
+
throw new Error(`files exceeds maximum of ${MAX_LOCAL_TRANSFER_FILES}`);
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
const safeRoots = getUploadSafeRoots().map(resolveExistingRoot);
|
|
2421
2554
|
return files.map((filePath) => {
|
|
2422
2555
|
const resolvedPath = path.resolve(filePath);
|
|
2423
2556
|
if (!fs.existsSync(resolvedPath)) {
|
|
2424
2557
|
throw new Error(`file does not exist: ${resolvedPath}`);
|
|
2425
2558
|
}
|
|
2426
|
-
const
|
|
2559
|
+
const realFilePath = fs.realpathSync(resolvedPath);
|
|
2560
|
+
const containingRoot = findContainingRoot(realFilePath, safeRoots);
|
|
2561
|
+
if (!containingRoot) {
|
|
2562
|
+
throw new Error(
|
|
2563
|
+
'file must be inside the browser session download directory or a CCS_BROWSER_UPLOAD_ROOTS entry'
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
assertNoSensitiveLocalPathSegments(realFilePath, containingRoot, 'file');
|
|
2567
|
+
|
|
2568
|
+
const stat = fs.statSync(realFilePath);
|
|
2427
2569
|
if (!stat.isFile()) {
|
|
2428
|
-
throw new Error(`file is not a regular file: ${
|
|
2570
|
+
throw new Error(`file is not a regular file: ${realFilePath}`);
|
|
2571
|
+
}
|
|
2572
|
+
if (stat.size > MAX_LOCAL_TRANSFER_FILE_BYTES) {
|
|
2573
|
+
throw new Error(
|
|
2574
|
+
`file exceeds maximum size of ${MAX_LOCAL_TRANSFER_FILE_BYTES} bytes: ${realFilePath}`
|
|
2575
|
+
);
|
|
2429
2576
|
}
|
|
2430
|
-
return
|
|
2577
|
+
return realFilePath;
|
|
2431
2578
|
});
|
|
2432
2579
|
}
|
|
2433
2580
|
|