@cloudbase/cloudbase-mcp 2.17.0 → 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 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 0.0.0.0 so Cloud IDE / VSCode Remote port-forward can connect; set INTERACTIVE_SERVER_HOST=127.0.0.1 for local-only
431
- const host = process.env.INTERACTIVE_SERVER_HOST ?? "0.0.0.0";
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();
@@ -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 0.0.0.0 so Cloud IDE / VSCode Remote port-forward can connect; set INTERACTIVE_SERVER_HOST=127.0.0.1 for local-only
1834
- const host = process.env.INTERACTIVE_SERVER_HOST ?? "0.0.0.0";
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();