Skip to content

Commit 127eae2

Browse files
committed
dont allow private ips
1 parent 9c158c5 commit 127eae2

File tree

4 files changed

+683
-10
lines changed

4 files changed

+683
-10
lines changed

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./crypto";
22
export * from "./url-validation";
3+
export * from "./target-validation";
34
export * from "./security";

src/lib/target-validation.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/**
2+
* Target validation utilities for Globalping measurements
3+
* Ensures only public endpoints are tested (Globalping API requirement)
4+
*/
5+
6+
/**
7+
* Check if an IPv4 address is in a private range (RFC1918)
8+
* Private ranges:
9+
* - 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
10+
* - 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
11+
* - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
12+
*
13+
* @param ip The IPv4 address to check
14+
* @returns True if the IP is in a private range
15+
*/
16+
export function isPrivateIPv4(ip: string): boolean {
17+
// Remove any IPv6 brackets if present
18+
const cleanIp = ip.replace(/^\[|\]$/g, "");
19+
20+
// Parse IPv4 address into octets
21+
const parts = cleanIp.split(".");
22+
if (parts.length !== 4) {
23+
return false;
24+
}
25+
26+
const octets = parts.map((part) => Number.parseInt(part, 10));
27+
28+
// Validate all octets are numbers 0-255
29+
if (octets.some((octet) => Number.isNaN(octet) || octet < 0 || octet > 255)) {
30+
return false;
31+
}
32+
33+
const [first, second] = octets;
34+
35+
// 10.0.0.0/8
36+
if (first === 10) {
37+
return true;
38+
}
39+
40+
// 172.16.0.0/12
41+
if (first === 172 && second >= 16 && second <= 31) {
42+
return true;
43+
}
44+
45+
// 192.168.0.0/16
46+
if (first === 192 && second === 168) {
47+
return true;
48+
}
49+
50+
return false;
51+
}
52+
53+
/**
54+
* Check if an IPv6 address is in a private range
55+
* Private ranges:
56+
* - fc00::/7 (Unique Local Addresses)
57+
* - fe80::/10 (Link-local addresses)
58+
*
59+
* @param ip The IPv6 address to check
60+
* @returns True if the IP is in a private range
61+
*/
62+
export function isPrivateIPv6(ip: string): boolean {
63+
// Remove brackets if present
64+
const cleanIp = ip.replace(/^\[|\]$/g, "").toLowerCase();
65+
66+
// fc00::/7 - Unique Local Addresses (fc00:: to fdff::)
67+
if (cleanIp.startsWith("fc") || cleanIp.startsWith("fd")) {
68+
return true;
69+
}
70+
71+
// fe80::/10 - Link-local addresses
72+
if (cleanIp.startsWith("fe8") || cleanIp.startsWith("fe9") || cleanIp.startsWith("fea") || cleanIp.startsWith("feb")) {
73+
return true;
74+
}
75+
76+
return false;
77+
}
78+
79+
/**
80+
* Check if an IPv4 address is a loopback address
81+
* Loopback range: 127.0.0.0/8
82+
*
83+
* @param ip The IPv4 address to check
84+
* @returns True if the IP is a loopback address
85+
*/
86+
export function isLoopbackIPv4(ip: string): boolean {
87+
const cleanIp = ip.replace(/^\[|\]$/g, "");
88+
const parts = cleanIp.split(".");
89+
90+
if (parts.length !== 4) {
91+
return false;
92+
}
93+
94+
const first = Number.parseInt(parts[0], 10);
95+
return first === 127;
96+
}
97+
98+
/**
99+
* Check if an IPv6 address is a loopback address
100+
* Loopback: ::1
101+
*
102+
* @param ip The IPv6 address to check
103+
* @returns True if the IP is a loopback address
104+
*/
105+
export function isLoopbackIPv6(ip: string): boolean {
106+
const cleanIp = ip.replace(/^\[|\]$/g, "").toLowerCase();
107+
108+
// ::1 is the only IPv6 loopback
109+
return cleanIp === "::1" || cleanIp === "0:0:0:0:0:0:0:1" || cleanIp === "0000:0000:0000:0000:0000:0000:0000:0001";
110+
}
111+
112+
/**
113+
* Check if an IPv4 address is a link-local address (APIPA)
114+
* Link-local range: 169.254.0.0/16
115+
*
116+
* @param ip The IPv4 address to check
117+
* @returns True if the IP is a link-local address
118+
*/
119+
export function isLinkLocalIPv4(ip: string): boolean {
120+
const cleanIp = ip.replace(/^\[|\]$/g, "");
121+
const parts = cleanIp.split(".");
122+
123+
if (parts.length !== 4) {
124+
return false;
125+
}
126+
127+
const octets = parts.map((part) => Number.parseInt(part, 10));
128+
129+
// Validate octets
130+
if (octets.some((octet) => Number.isNaN(octet) || octet < 0 || octet > 255)) {
131+
return false;
132+
}
133+
134+
// 169.254.0.0/16
135+
return octets[0] === 169 && octets[1] === 254;
136+
}
137+
138+
/**
139+
* Check if a target is a localhost domain
140+
* Matches: localhost, *.localhost
141+
*
142+
* @param target The target to check
143+
* @returns True if the target is a localhost domain
144+
*/
145+
export function isLocalhostDomain(target: string): boolean {
146+
const lower = target.toLowerCase();
147+
return lower === "localhost" || lower.endsWith(".localhost");
148+
}
149+
150+
/**
151+
* Validate if a target is a public endpoint
152+
* Globalping only supports public endpoints - no private IPs, localhost, or link-local addresses
153+
*
154+
* @param target The target domain or IP address to validate
155+
* @returns Validation result with reason if invalid
156+
*/
157+
export function isPublicTarget(target: string): { valid: boolean; reason?: string } {
158+
if (!target || target.trim() === "") {
159+
return { valid: false, reason: "Target cannot be empty" };
160+
}
161+
162+
const cleanTarget = target.trim();
163+
164+
// Check for localhost domain patterns
165+
if (isLocalhostDomain(cleanTarget)) {
166+
return {
167+
valid: false,
168+
reason: `'${cleanTarget}' is a localhost domain`,
169+
};
170+
}
171+
172+
// Try to determine if it's an IP address
173+
// IPv6 addresses may be in brackets
174+
const ipToCheck = cleanTarget.replace(/^\[|\]$/g, "");
175+
176+
// Check if it looks like an IPv4 address (contains only digits and dots)
177+
const ipv4Pattern = /^[\d.]+$/;
178+
if (ipv4Pattern.test(ipToCheck)) {
179+
// Check IPv4 loopback
180+
if (isLoopbackIPv4(ipToCheck)) {
181+
return {
182+
valid: false,
183+
reason: `${ipToCheck} is a loopback address (127.0.0.0/8)`,
184+
};
185+
}
186+
187+
// Check IPv4 private ranges
188+
if (isPrivateIPv4(ipToCheck)) {
189+
return {
190+
valid: false,
191+
reason: `${ipToCheck} is a private IPv4 address (RFC1918)`,
192+
};
193+
}
194+
195+
// Check IPv4 link-local
196+
if (isLinkLocalIPv4(ipToCheck)) {
197+
return {
198+
valid: false,
199+
reason: `${ipToCheck} is a link-local address (169.254.0.0/16)`,
200+
};
201+
}
202+
}
203+
204+
// Check if it looks like an IPv6 address (contains colons)
205+
if (ipToCheck.includes(":")) {
206+
// Check IPv6 loopback
207+
if (isLoopbackIPv6(ipToCheck)) {
208+
return {
209+
valid: false,
210+
reason: `${ipToCheck} is the IPv6 loopback address`,
211+
};
212+
}
213+
214+
// Check IPv6 private ranges
215+
if (isPrivateIPv6(ipToCheck)) {
216+
return {
217+
valid: false,
218+
reason: `${ipToCheck} is a private IPv6 address`,
219+
};
220+
}
221+
}
222+
223+
// If it doesn't match any private/local patterns, assume it's public
224+
// (could be a domain name or a public IP)
225+
return { valid: true };
226+
}

src/mcp/tools.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { runMeasurement, getLocations, getRateLimits } from "../api";
66
import { parseLocations, formatMeasurementSummary } from "./helpers";
77
import type { GlobalpingMCP } from "../index";
88
import { maskToken } from "../auth";
9+
import { isPublicTarget } from "../lib";
910

1011
/**
1112
* Helper to wrap tool execution with error handling
@@ -41,14 +42,14 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
4142
{
4243
title: "Ping Test",
4344
description:
44-
"Measure network latency, packet loss, and reachability to a target (domain or IP) from globally distributed probes. Use this tool to check if a server is online, debug connection issues, or assess global performance.",
45+
"Measure network latency, packet loss, and reachability to a target (domain or IP) from globally distributed probes. Use this tool to check if a server is online, debug connection issues, or assess global performance. Note: Only public endpoints are supported. Private networks cannot be tested.",
4546
annotations: {
4647
readOnlyHint: true,
4748
},
4849
inputSchema: {
4950
target: z
5051
.string()
51-
.describe("Domain name or IP to test (e.g., 'google.com', '1.1.1.1')"),
52+
.describe("Public domain name or IP address to test (e.g., 'google.com', '1.1.1.1'). Private IPs (RFC1918), localhost, and link-local addresses are not supported."),
5253
locations: z
5354
.union([z.array(z.string()), z.string()])
5455
.optional()
@@ -74,6 +75,14 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
7475
},
7576
async ({ target, locations, limit, packets }) => {
7677
return handleToolExecution(async () => {
78+
// Validate target is public
79+
const validation = isPublicTarget(target);
80+
if (!validation.valid) {
81+
throw new Error(
82+
`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
83+
);
84+
}
85+
7786
const token = getToken();
7887
const parsedLocations = parseLocations(locations);
7988

@@ -129,14 +138,14 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
129138
{
130139
title: "Traceroute Test",
131140
description:
132-
"Trace the network path to a target (domain or IP) from global locations. Use this tool to identify where packets are being dropped, analyze routing paths, or pinpoint latency sources in the network.",
141+
"Trace the network path to a target (domain or IP) from global locations. Use this tool to identify where packets are being dropped, analyze routing paths, or pinpoint latency sources in the network. Note: Only public endpoints are supported. Private networks cannot be tested.",
133142
annotations: {
134143
readOnlyHint: true,
135144
},
136145
inputSchema: {
137146
target: z
138147
.string()
139-
.describe("Domain name or IP to test (e.g., 'cloudflare.com', '1.1.1.1')"),
148+
.describe("Public domain name or IP address to test (e.g., 'cloudflare.com', '1.1.1.1'). Private IPs (RFC1918), localhost, and link-local addresses are not supported."),
140149
locations: z
141150
.union([z.array(z.string()), z.string()])
142151
.optional()
@@ -169,6 +178,14 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
169178
},
170179
async ({ target, locations, limit, protocol, port }) => {
171180
return handleToolExecution(async () => {
181+
// Validate target is public
182+
const validation = isPublicTarget(target);
183+
if (!validation.valid) {
184+
throw new Error(
185+
`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
186+
);
187+
}
188+
172189
const token = getToken();
173190
const parsedLocations = parseLocations(locations);
174191

@@ -224,12 +241,12 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
224241
{
225242
title: "DNS Lookup",
226243
description:
227-
"Resolve DNS records (A, AAAA, MX, etc.) for a domain from global locations. Use this tool to verify DNS propagation, troubleshoot resolution failures, or check if users in different regions are seeing the correct records.",
244+
"Resolve DNS records (A, AAAA, MX, etc.) for a domain from global locations. Use this tool to verify DNS propagation, troubleshoot resolution failures, or check if users in different regions are seeing the correct records. Note: Only public endpoints are supported. Private networks cannot be tested.",
228245
annotations: {
229246
readOnlyHint: true,
230247
},
231248
inputSchema: {
232-
target: z.string().describe("Domain name to resolve (e.g., 'google.com')"),
249+
target: z.string().describe("Public domain name to resolve (e.g., 'google.com'). Private domains, localhost, and link-local addresses are not supported."),
233250
locations: z
234251
.union([z.array(z.string()), z.string()])
235252
.optional()
@@ -285,6 +302,14 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
285302
},
286303
async ({ target, locations, limit, queryType, resolver, trace }) => {
287304
return handleToolExecution(async () => {
305+
// Validate target is public
306+
const validation = isPublicTarget(target);
307+
if (!validation.valid) {
308+
throw new Error(
309+
`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
310+
);
311+
}
312+
288313
const token = getToken();
289314
const parsedLocations = parseLocations(locations);
290315

@@ -343,15 +368,15 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
343368
{
344369
title: "MTR Test",
345370
description:
346-
"Run an MTR (My Traceroute) diagnostic, which combines Ping and Traceroute. Use this tool to analyze packet loss and latency trends at every hop in the network path over time, helpful for spotting intermittent issues.",
371+
"Run an MTR (My Traceroute) diagnostic, which combines Ping and Traceroute. Use this tool to analyze packet loss and latency trends at every hop in the network path over time, helpful for spotting intermittent issues. Note: Only public endpoints are supported. Private networks cannot be tested.",
347372
annotations: {
348373
readOnlyHint: true,
349374
},
350375
inputSchema: {
351376
target: z
352377
.string()
353378
.min(1)
354-
.describe("Destination hostname or IP to run the MTR against"),
379+
.describe("Public destination hostname or IP address to run the MTR against. Private IPs (RFC1918), localhost, and link-local addresses are not supported."),
355380
locations: z
356381
.union([z.array(z.string()), z.string()])
357382
.optional()
@@ -387,6 +412,14 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
387412
},
388413
async ({ target, locations, limit, protocol, port, packets }) => {
389414
return handleToolExecution(async () => {
415+
// Validate target is public
416+
const validation = isPublicTarget(target);
417+
if (!validation.valid) {
418+
throw new Error(
419+
`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
420+
);
421+
}
422+
390423
const token = getToken();
391424
const parsedLocations = parseLocations(locations);
392425

@@ -443,12 +476,12 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
443476
{
444477
title: "HTTP Request",
445478
description:
446-
"Send HTTP/HTTPS requests (GET, HEAD or OPTIONS) to a URL from global locations. Use this tool to check website uptime, verify response status codes, analyze timing (TTFB, download), and debug CDN or caching issues.",
479+
"Send HTTP/HTTPS requests (GET, HEAD or OPTIONS) to a URL from global locations. Use this tool to check website uptime, verify response status codes, analyze timing (TTFB, download), and debug CDN or caching issues. Note: Only public endpoints are supported. Private networks cannot be tested.",
447480
annotations: {
448481
readOnlyHint: true,
449482
},
450483
inputSchema: {
451-
target: z.string().describe("Domain name or IP to test (e.g., 'example.com')"),
484+
target: z.string().describe("Public domain name or IP address to test (e.g., 'example.com'). Private IPs (RFC1918), localhost, and link-local addresses are not supported."),
452485
locations: z
453486
.union([z.array(z.string()), z.string()])
454487
.optional()
@@ -495,6 +528,14 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
495528
},
496529
async ({ target, locations, limit, method, protocol, path, query, port }) => {
497530
return handleToolExecution(async () => {
531+
// Validate target is public
532+
const validation = isPublicTarget(target);
533+
if (!validation.valid) {
534+
throw new Error(
535+
`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
536+
);
537+
}
538+
498539
const token = getToken();
499540
const parsedLocations = parseLocations(locations);
500541

0 commit comments

Comments
 (0)