@cloudbase/cloudbase-mcp 2.17.0-beta.2 → 2.17.1
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/341.index.js +92 -2
- package/dist/602.index.cjs +92 -2
- package/dist/cli.cjs +3 -3
- package/dist/index.cjs +43 -388
- package/dist/index.d.ts +2 -0
- package/dist/index.js +42 -386
- package/package.json +1 -1
package/dist/341.index.js
CHANGED
|
@@ -26,6 +26,85 @@ function isVSCodeEnvironment() {
|
|
|
26
26
|
process.env.VSCODE_PID ||
|
|
27
27
|
process.env.TERM_PROGRAM === "vscode");
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Validates that a URL is safe to open, blocking SSRF targets.
|
|
31
|
+
* Only allows http/https schemes and rejects private/reserved IP ranges
|
|
32
|
+
* and sensitive hostnames.
|
|
33
|
+
*/
|
|
34
|
+
function isUrlSafeToOpen(url) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = new URL(url);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return { safe: false, reason: "Invalid URL format" };
|
|
41
|
+
}
|
|
42
|
+
// Only allow http and https schemes
|
|
43
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
44
|
+
return { safe: false, reason: `Unsupported scheme: ${parsed.protocol}` };
|
|
45
|
+
}
|
|
46
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
47
|
+
// Block common sensitive / internal hostnames
|
|
48
|
+
const blockedHostnames = [
|
|
49
|
+
"localhost",
|
|
50
|
+
"localhost.localdomain",
|
|
51
|
+
"ip6-localhost",
|
|
52
|
+
"ip6-loopback",
|
|
53
|
+
"metadata.google.internal",
|
|
54
|
+
"metadata.internal",
|
|
55
|
+
];
|
|
56
|
+
if (blockedHostnames.includes(hostname)) {
|
|
57
|
+
return { safe: false, reason: `Blocked hostname: ${hostname}` };
|
|
58
|
+
}
|
|
59
|
+
// Block hostnames ending with .internal or .local
|
|
60
|
+
if (hostname.endsWith(".internal") || hostname.endsWith(".local")) {
|
|
61
|
+
return { safe: false, reason: `Blocked internal hostname: ${hostname}` };
|
|
62
|
+
}
|
|
63
|
+
// Block IP addresses in private/reserved ranges (RFC 1918, loopback, link-local, etc.)
|
|
64
|
+
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
65
|
+
const match = ipv4Regex.exec(hostname);
|
|
66
|
+
if (match) {
|
|
67
|
+
const octets = [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10), parseInt(match[4], 10)];
|
|
68
|
+
// Loopback: 127.0.0.0/8
|
|
69
|
+
if (octets[0] === 127) {
|
|
70
|
+
return { safe: false, reason: "Blocked loopback IP address" };
|
|
71
|
+
}
|
|
72
|
+
// Link-local: 169.254.0.0/16
|
|
73
|
+
if (octets[0] === 169 && octets[1] === 254) {
|
|
74
|
+
return { safe: false, reason: "Blocked link-local IP address" };
|
|
75
|
+
}
|
|
76
|
+
// RFC 1918 private: 10.0.0.0/8
|
|
77
|
+
if (octets[0] === 10) {
|
|
78
|
+
return { safe: false, reason: "Blocked private IP address (10.x.x.x)" };
|
|
79
|
+
}
|
|
80
|
+
// RFC 1918 private: 172.16.0.0/12
|
|
81
|
+
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
|
|
82
|
+
return { safe: false, reason: "Blocked private IP address (172.16-31.x.x)" };
|
|
83
|
+
}
|
|
84
|
+
// RFC 1918 private: 192.168.0.0/16
|
|
85
|
+
if (octets[0] === 192 && octets[1] === 168) {
|
|
86
|
+
return { safe: false, reason: "Blocked private IP address (192.168.x.x)" };
|
|
87
|
+
}
|
|
88
|
+
// Broadcast: 0.0.0.0
|
|
89
|
+
if (octets[0] === 0 && octets[1] === 0 && octets[2] === 0 && octets[3] === 0) {
|
|
90
|
+
return { safe: false, reason: "Blocked broadcast address" };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Block IPv6 loopback and link-local
|
|
94
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
|
95
|
+
const ipv6 = hostname.slice(1, -1).toLowerCase();
|
|
96
|
+
if (ipv6 === "::1" || ipv6 === "0:0:0:0:0:0:0:1" || ipv6 === "::" || ipv6 === "0:0:0:0:0:0:0:0") {
|
|
97
|
+
return { safe: false, reason: "Blocked IPv6 loopback/reserved address" };
|
|
98
|
+
}
|
|
99
|
+
if (ipv6.startsWith("fe80:")) {
|
|
100
|
+
return { safe: false, reason: "Blocked IPv6 link-local address" };
|
|
101
|
+
}
|
|
102
|
+
if (ipv6.startsWith("fc") || ipv6.startsWith("fd")) {
|
|
103
|
+
return { safe: false, reason: "Blocked IPv6 unique-local address" };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { safe: true };
|
|
107
|
+
}
|
|
29
108
|
function parseBrowserCommand(browser) {
|
|
30
109
|
const parts = browser.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
31
110
|
return parts.map((item) => item.replace(/^"(.*)"$/, "$1"));
|
|
@@ -249,6 +328,17 @@ class InteractiveServer {
|
|
|
249
328
|
res.status(400).json({ success: false, error: "URL is required" });
|
|
250
329
|
return;
|
|
251
330
|
}
|
|
331
|
+
if (typeof url !== "string") {
|
|
332
|
+
res.status(400).json({ success: false, error: "URL must be a string" });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Validate URL to prevent SSRF
|
|
336
|
+
const validation = isUrlSafeToOpen(url);
|
|
337
|
+
if (!validation.safe) {
|
|
338
|
+
(0, logger_js_1.warn)(`Blocked open-url request for unsafe URL: ${url} (${validation.reason})`);
|
|
339
|
+
res.status(403).json({ success: false, error: `URL not allowed: ${validation.reason}` });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
252
342
|
(0, logger_js_1.info)("Received open URL request", { url });
|
|
253
343
|
try {
|
|
254
344
|
// Pass mcpServer directly - it's an ExtendedMcpServer instance with server property
|
|
@@ -427,8 +517,8 @@ class InteractiveServer {
|
|
|
427
517
|
reject(err);
|
|
428
518
|
}
|
|
429
519
|
};
|
|
430
|
-
// Host: default
|
|
431
|
-
const host = process.env.INTERACTIVE_SERVER_HOST ?? "
|
|
520
|
+
// Host: default 127.0.0.1 for security (prevents SSRF from remote access); set INTERACTIVE_SERVER_HOST=0.0.0.0 to allow remote connections
|
|
521
|
+
const host = process.env.INTERACTIVE_SERVER_HOST ?? "127.0.0.1";
|
|
432
522
|
// 设置成功监听处理
|
|
433
523
|
const listeningHandler = () => {
|
|
434
524
|
const address = this.server.address();
|
package/dist/602.index.cjs
CHANGED
|
@@ -1429,6 +1429,85 @@ function isVSCodeEnvironment() {
|
|
|
1429
1429
|
process.env.VSCODE_PID ||
|
|
1430
1430
|
process.env.TERM_PROGRAM === "vscode");
|
|
1431
1431
|
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Validates that a URL is safe to open, blocking SSRF targets.
|
|
1434
|
+
* Only allows http/https schemes and rejects private/reserved IP ranges
|
|
1435
|
+
* and sensitive hostnames.
|
|
1436
|
+
*/
|
|
1437
|
+
function isUrlSafeToOpen(url) {
|
|
1438
|
+
let parsed;
|
|
1439
|
+
try {
|
|
1440
|
+
parsed = new URL(url);
|
|
1441
|
+
}
|
|
1442
|
+
catch {
|
|
1443
|
+
return { safe: false, reason: "Invalid URL format" };
|
|
1444
|
+
}
|
|
1445
|
+
// Only allow http and https schemes
|
|
1446
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1447
|
+
return { safe: false, reason: `Unsupported scheme: ${parsed.protocol}` };
|
|
1448
|
+
}
|
|
1449
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1450
|
+
// Block common sensitive / internal hostnames
|
|
1451
|
+
const blockedHostnames = [
|
|
1452
|
+
"localhost",
|
|
1453
|
+
"localhost.localdomain",
|
|
1454
|
+
"ip6-localhost",
|
|
1455
|
+
"ip6-loopback",
|
|
1456
|
+
"metadata.google.internal",
|
|
1457
|
+
"metadata.internal",
|
|
1458
|
+
];
|
|
1459
|
+
if (blockedHostnames.includes(hostname)) {
|
|
1460
|
+
return { safe: false, reason: `Blocked hostname: ${hostname}` };
|
|
1461
|
+
}
|
|
1462
|
+
// Block hostnames ending with .internal or .local
|
|
1463
|
+
if (hostname.endsWith(".internal") || hostname.endsWith(".local")) {
|
|
1464
|
+
return { safe: false, reason: `Blocked internal hostname: ${hostname}` };
|
|
1465
|
+
}
|
|
1466
|
+
// Block IP addresses in private/reserved ranges (RFC 1918, loopback, link-local, etc.)
|
|
1467
|
+
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
1468
|
+
const match = ipv4Regex.exec(hostname);
|
|
1469
|
+
if (match) {
|
|
1470
|
+
const octets = [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10), parseInt(match[4], 10)];
|
|
1471
|
+
// Loopback: 127.0.0.0/8
|
|
1472
|
+
if (octets[0] === 127) {
|
|
1473
|
+
return { safe: false, reason: "Blocked loopback IP address" };
|
|
1474
|
+
}
|
|
1475
|
+
// Link-local: 169.254.0.0/16
|
|
1476
|
+
if (octets[0] === 169 && octets[1] === 254) {
|
|
1477
|
+
return { safe: false, reason: "Blocked link-local IP address" };
|
|
1478
|
+
}
|
|
1479
|
+
// RFC 1918 private: 10.0.0.0/8
|
|
1480
|
+
if (octets[0] === 10) {
|
|
1481
|
+
return { safe: false, reason: "Blocked private IP address (10.x.x.x)" };
|
|
1482
|
+
}
|
|
1483
|
+
// RFC 1918 private: 172.16.0.0/12
|
|
1484
|
+
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
|
|
1485
|
+
return { safe: false, reason: "Blocked private IP address (172.16-31.x.x)" };
|
|
1486
|
+
}
|
|
1487
|
+
// RFC 1918 private: 192.168.0.0/16
|
|
1488
|
+
if (octets[0] === 192 && octets[1] === 168) {
|
|
1489
|
+
return { safe: false, reason: "Blocked private IP address (192.168.x.x)" };
|
|
1490
|
+
}
|
|
1491
|
+
// Broadcast: 0.0.0.0
|
|
1492
|
+
if (octets[0] === 0 && octets[1] === 0 && octets[2] === 0 && octets[3] === 0) {
|
|
1493
|
+
return { safe: false, reason: "Blocked broadcast address" };
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// Block IPv6 loopback and link-local
|
|
1497
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
|
1498
|
+
const ipv6 = hostname.slice(1, -1).toLowerCase();
|
|
1499
|
+
if (ipv6 === "::1" || ipv6 === "0:0:0:0:0:0:0:1" || ipv6 === "::" || ipv6 === "0:0:0:0:0:0:0:0") {
|
|
1500
|
+
return { safe: false, reason: "Blocked IPv6 loopback/reserved address" };
|
|
1501
|
+
}
|
|
1502
|
+
if (ipv6.startsWith("fe80:")) {
|
|
1503
|
+
return { safe: false, reason: "Blocked IPv6 link-local address" };
|
|
1504
|
+
}
|
|
1505
|
+
if (ipv6.startsWith("fc") || ipv6.startsWith("fd")) {
|
|
1506
|
+
return { safe: false, reason: "Blocked IPv6 unique-local address" };
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return { safe: true };
|
|
1510
|
+
}
|
|
1432
1511
|
function parseBrowserCommand(browser) {
|
|
1433
1512
|
const parts = browser.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
1434
1513
|
return parts.map((item) => item.replace(/^"(.*)"$/, "$1"));
|
|
@@ -1652,6 +1731,17 @@ class InteractiveServer {
|
|
|
1652
1731
|
res.status(400).json({ success: false, error: "URL is required" });
|
|
1653
1732
|
return;
|
|
1654
1733
|
}
|
|
1734
|
+
if (typeof url !== "string") {
|
|
1735
|
+
res.status(400).json({ success: false, error: "URL must be a string" });
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
// Validate URL to prevent SSRF
|
|
1739
|
+
const validation = isUrlSafeToOpen(url);
|
|
1740
|
+
if (!validation.safe) {
|
|
1741
|
+
(0, logger_js_1.warn)(`Blocked open-url request for unsafe URL: ${url} (${validation.reason})`);
|
|
1742
|
+
res.status(403).json({ success: false, error: `URL not allowed: ${validation.reason}` });
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1655
1745
|
(0, logger_js_1.info)("Received open URL request", { url });
|
|
1656
1746
|
try {
|
|
1657
1747
|
// Pass mcpServer directly - it's an ExtendedMcpServer instance with server property
|
|
@@ -1830,8 +1920,8 @@ class InteractiveServer {
|
|
|
1830
1920
|
reject(err);
|
|
1831
1921
|
}
|
|
1832
1922
|
};
|
|
1833
|
-
// Host: default
|
|
1834
|
-
const host = process.env.INTERACTIVE_SERVER_HOST ?? "
|
|
1923
|
+
// Host: default 127.0.0.1 for security (prevents SSRF from remote access); set INTERACTIVE_SERVER_HOST=0.0.0.0 to allow remote connections
|
|
1924
|
+
const host = process.env.INTERACTIVE_SERVER_HOST ?? "127.0.0.1";
|
|
1835
1925
|
// 设置成功监听处理
|
|
1836
1926
|
const listeningHandler = () => {
|
|
1837
1927
|
const address = this.server.address();
|