Skip to content

Conversation

@jimaek
Copy link
Member

@jimaek jimaek commented Nov 29, 2025

MCPcat already logged users having issues with their agents trying to run tests against LAN ips like 10.0.0.x.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Nov 29, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
globalping-mcp-server ff4f90f Nov 29 2025, 03:47 PM

@coderabbitai
Copy link

coderabbitai bot commented Nov 29, 2025

Walkthrough

Adds a new target-validation module that detects localhost, loopback, private IPv4/IPv6, and link-local addresses and exposes an isPublicTarget function returning { valid: boolean; reason?: string }. Re-exports the module from the library index. Integrates isPublicTarget checks into tool execution paths in src/mcp/tools.ts and updates tool descriptions to state that only public endpoints are supported. Adds comprehensive unit tests covering classification and boundary cases.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly relates to the main change—preventing private IPs from being tested—which is the core functionality added across the codebase.
Description check ✅ Passed The description provides relevant context explaining the business motivation: MCPcat users were experiencing issues running tests against LAN IPs, which this PR resolves.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch privateips

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/lib/index.ts (1)

1-4: Be deliberate about exposing all target-validation helpers via the public barrel

Re-exporting ./target-validation from the main lib index makes all helpers (e.g. isPrivateIPv4, isPrivateIPv6, etc.) part of your public API surface. If you only intend isPublicTarget to be a supported entry point, consider exporting just that (or re-exporting named symbols explicitly) to avoid committing to the stability of all the lower-level helpers and reduce the chance of external callers depending on internal details.

test/unit/lib/target-validation.test.ts (1)

210-239: Augment tests for localhost FQDNs and IPv4‑mapped IPv6 edge cases

Given the goal of hardening against private/localhost targets, the current tests are missing a few important cases:

  • localhost. and *.localhost. (FQDNs with a trailing dot) should be treated as localhost domains if you adopt the normalization suggested in isLocalhostDomain.
  • IPv4‑mapped IPv6 literals such as ::ffff:10.0.0.1 and 0:0:0:0:0:ffff:192.168.1.1 should be explicitly rejected when the embedded IPv4 part is private, and accepted when it’s public.

You can extend the suite along these lines:

 describe("isLocalhostDomain", () => {
@@
 	it("should not detect empty or non-localhost strings", () => {
 		expect(isLocalhostDomain("")).toBe(false);
 		expect(isLocalhostDomain("example.com")).toBe(false);
 		expect(isLocalhostDomain("google.com")).toBe(false);
 	});
+
+	it("should detect localhost FQDNs with trailing dot", () => {
+		expect(isLocalhostDomain("localhost.")).toBe(true);
+		expect(isLocalhostDomain("app.localhost.")).toBe(true);
+	});
 });
@@
 describe("isPublicTarget", () => {
@@
 	describe("localhost domains", () => {
@@
 		it("should reject *.localhost", () => {
 			const result = isPublicTarget("app.localhost");
 			expect(result.valid).toBe(false);
 			expect(result.reason).toContain("localhost domain");
 		});
+
+		it("should reject localhost with trailing dot", () => {
+			const result = isPublicTarget("localhost.");
+			expect(result.valid).toBe(false);
+			expect(result.reason).toContain("localhost domain");
+		});
+
+		it("should reject *.localhost with trailing dot", () => {
+			const result = isPublicTarget("app.localhost.");
+			expect(result.valid).toBe(false);
+			expect(result.reason).toContain("localhost domain");
+		});
 	});
@@
+	describe("IPv4-mapped IPv6 addresses", () => {
+		it("should reject IPv4-mapped private IPv4 addresses", () => {
+			const result = isPublicTarget("::ffff:10.0.0.1");
+			expect(result.valid).toBe(false);
+			expect(result.reason).toContain("private IPv4");
+		});
+
+		it("should accept IPv4-mapped public IPv4 addresses", () => {
+			expect(isPublicTarget("::ffff:1.1.1.1").valid).toBe(true);
+		});
+	});

These cases will lock in the intended behavior and protect against regressions as the validation logic evolves.

Also applies to: 241-405

src/mcp/tools.ts (2)

45-53: Avoid repeating the same target-validation/error block in every tool

The isPublicTarget check plus the long error message is duplicated in each tool handler. This makes it easy for behavior or wording to drift if one call site is updated but others are not, and increases the chance that a new tool forgets to add the validation at all.

You could centralize this into a small helper in this module, e.g.:

function assertPublicTarget(target: string) {
	const validation = isPublicTarget(target);

	if (!validation.valid) {
		throw new Error(
			`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
		);
	}
}

Then each tool handler only needs:

assertPublicTarget(target);

This keeps the behavior and messaging consistent and makes future policy changes (e.g., expanding blocked ranges) a single edit instead of five.

Also applies to: 78-85, 141-149, 181-188, 244-250, 305-312, 371-380, 415-422, 479-485, 531-538


244-289: Custom DNS resolver parameter currently bypasses the public-target validation

In the DNS tool, only target is validated with isPublicTarget, while resolver is passed straight through into measurementOptions:

measurementOptions: {
	query: { type: queryType || "A" },
	resolver,
	trace: trace || false,
}

If the goal is “no private IPs / localhost / link-local anywhere in a measurement”, this lets callers still point probes at private or localhost resolvers (e.g., 192.168.0.1, fc00::1, localhost) even though the main target is constrained.

If resolvers are also supposed to be public-only, consider validating them too, e.g.:

 	async ({ target, locations, limit, queryType, resolver, trace }) => {
 		return handleToolExecution(async () => {
 			// Validate target is public
-			const validation = isPublicTarget(target);
-			if (!validation.valid) {
-				throw new Error(
-					`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
-				);
-			}
+			const validation = isPublicTarget(target);
+			if (!validation.valid) {
+				throw new Error(
+					`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
+				);
+			}
+
+			if (resolver) {
+				const resolverValidation = isPublicTarget(resolver);
+				if (!resolverValidation.valid) {
+					throw new Error(
+						`Invalid resolver: ${resolverValidation.reason}. Globalping only supports public resolvers. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
+					);
+				}
+			}

If, on the other hand, private resolvers are intentionally allowed (e.g., to hit operator-controlled infrastructure that isn’t globally routable), it would be good to clarify that exception in the description and docs so users don’t assume the “public-only” rule applies uniformly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c158c5 and 127eae2.

📒 Files selected for processing (4)
  • src/lib/index.ts (1 hunks)
  • src/lib/target-validation.ts (1 hunks)
  • src/mcp/tools.ts (11 hunks)
  • test/unit/lib/target-validation.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{vue,ts,js}

⚙️ CodeRabbit configuration file

We use Nuxt with auto imports enabled. Don't warn about missing imports.

Files:

  • src/lib/index.ts
  • test/unit/lib/target-validation.test.ts
  • src/mcp/tools.ts
  • src/lib/target-validation.ts
🧬 Code graph analysis (2)
test/unit/lib/target-validation.test.ts (1)
src/lib/target-validation.ts (7)
  • isPrivateIPv4 (16-51)
  • isPrivateIPv6 (62-77)
  • isLoopbackIPv4 (86-96)
  • isLoopbackIPv6 (105-110)
  • isLinkLocalIPv4 (119-136)
  • isLocalhostDomain (145-148)
  • isPublicTarget (157-226)
src/mcp/tools.ts (1)
src/lib/target-validation.ts (1)
  • isPublicTarget (157-226)
🔍 Remote MCP

I have successfully processed the comprehensive pull request documentation. Let me provide you with relevant context about the changes in this PR:

PR Context Summary

Pull Request: "Dont allow private ips" (#28)

This PR introduces critical validation to prevent private IP addresses from being used in network measurements through the Globalping MCP Server. Based on the extensive documentation, here are the key insights:

Changes Overview

The PR adds three major components:

  1. Target Validation Module (src/lib/target-validation.ts)

    • Functions to detect private IPv4/IPv6, loopback, and link-local addresses
    • Localhost domain detection for various formats (localhost, .localhost, 127.0.0.1, ::1)
    • Public target validation that returns { valid: boolean, reason?: string } with specific failure reasons
  2. Integration into MCP Tools (src/mcp/tools.ts)

    • Runtime validation on every Globalping tool registration to enforce public-only endpoints
    • Error responses when private IPs/localhost/link-local addresses are detected
    • Updated tool descriptions to explicitly note public-only restrictions
  3. Comprehensive Test Coverage (test/unit/lib/target-validation.test.ts)

    • RFC1918 private range validation (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
    • IPv6 unique local and link-local address detection
    • Localhost and .localhost TLD handling
    • Boundary cases and mixed format handling

Security Rationale

The PR addresses MCPcat's logged instances of users attempting to run tests against LAN IPs (10.0.0.x), which represents a significant security concern. Private IP testing could expose internal network infrastructure or be used maliciously.

Integration Points

According to the documentation, the validation integrates at multiple layers:

  • [::Authentication::] OAuth and API token flows remain unchanged; validation happens post-authentication
  • [::MCP Server::] Validation occurs before token retrieval and measurement execution in the request flow
  • [::API Client::] The sanitized token is still used for all Globalping API calls to public targets

Code Quality Standards

The codebase enforces:

  • Biome-based formatting (4-space indent, 100-char line width)
  • Comprehensive TypeScript strict mode
  • Automated import organization
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: globalping-mcp-server

Comment on lines +145 to +148
export function isLocalhostDomain(target: string): boolean {
const lower = target.toLowerCase();
return lower === "localhost" || lower.endsWith(".localhost");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Localhost FQDNs and IPv4‑mapped IPv6 addresses currently bypass the “public target” guard

Two notable edge cases slip through isPublicTarget right now:

  1. Localhost with a trailing root dot
    Domains such as "localhost." and "app.localhost." are standard FQDN representations of the localhost namespace, but isLocalhostDomain only matches "localhost" and *.localhost without a trailing dot. These inputs are therefore treated as public and accepted.

  2. IPv4‑mapped IPv6 literals with private IPv4 parts
    Addresses like ::ffff:10.0.0.1 or 0:0:0:0:0:ffff:192.168.1.1 are IPv6 forms of private IPv4 addresses. The current logic only checks IPv6 ULA/link‑local prefixes (fc, fd, fe8feb) and does not look for IPv4‑mapped forms, so these will be classified as public even though they conceptually target the same private space you’re trying to block.

If the underlying Globalping stack accepts IPv4‑mapped IPv6 addresses (common in many networking stacks), this creates a straightforward way to sidestep the private‑IP restriction.

You can tighten both cases with small changes:

-export function isLocalhostDomain(target: string): boolean {
-	const lower = target.toLowerCase();
-	return lower === "localhost" || lower.endsWith(".localhost");
-}
+export function isLocalhostDomain(target: string): boolean {
+	const lower = target.toLowerCase();
+
+	// Normalize a trailing root dot (e.g., "localhost.", "app.localhost.")
+	const normalized = lower.endsWith(".") ? lower.slice(0, -1) : lower;
+
+	return normalized === "localhost" || normalized.endsWith(".localhost");
+}

And in isPublicTarget, normalize IPv4‑mapped IPv6 into the IPv4 path so the existing private/loopback/link‑local checks apply:

 	// Try to determine if it's an IP address
 	// IPv6 addresses may be in brackets
 	const ipToCheck = cleanTarget.replace(/^\[|\]$/g, "");
 
-	// Check if it looks like an IPv4 address (contains only digits and dots)
-	const ipv4Pattern = /^[\d.]+$/;
-	if (ipv4Pattern.test(ipToCheck)) {
+	// Handle IPv4-mapped IPv6 like ::ffff:10.0.0.1 or 0:0:0:0:0:ffff:10.0.0.1
+	const ipv4MappedMatch = ipToCheck.match(
+		/^(?:\:\:ffff:|0:0:0:0:0:ffff:)(\d+\.\d+\.\d+\.\d+)$/i,
+	);
+	const ipv4Candidate = ipv4MappedMatch ? ipv4MappedMatch[1] : ipToCheck;
+
+	// Check if it looks like an IPv4 address (contains only digits and dots)
+	const ipv4Pattern = /^[\d.]+$/;
+	if (ipv4Pattern.test(ipv4Candidate)) {
 		// Check IPv4 loopback
-		if (isLoopbackIPv4(ipToCheck)) {
+		if (isLoopbackIPv4(ipv4Candidate)) {
 			return {
 				valid: false,
-				reason: `${ipToCheck} is a loopback address (127.0.0.0/8)`,
+				reason: `${ipv4Candidate} is a loopback address (127.0.0.0/8)`,
 			};
 		}
 
 		// Check IPv4 private ranges
-		if (isPrivateIPv4(ipToCheck)) {
+		if (isPrivateIPv4(ipv4Candidate)) {
 			return {
 				valid: false,
-				reason: `${ipToCheck} is a private IPv4 address (RFC1918)`,
+				reason: `${ipv4Candidate} is a private IPv4 address (RFC1918)`,
 			};
 		}
 
 		// Check IPv4 link-local
-		if (isLinkLocalIPv4(ipToCheck)) {
+		if (isLinkLocalIPv4(ipv4Candidate)) {
 			return {
 				valid: false,
-				reason: `${ipToCheck} is a link-local address (169.254.0.0/16)`,
+				reason: `${ipv4Candidate} is a link-local address (169.254.0.0/16)`,
 			};
 		}
 	}

Separately, if target can ever include a port (e.g., "10.0.0.1:80"), that form will currently bypass the IPv4 checks because of the colon and be treated as IPv6‑ish, eventually classified as public. Either enforce a “host only” invariant at all call sites or strip an IPv4‑style host:port before running this logic to avoid that loophole as well.

Also applies to: 157-225

🤖 Prompt for AI Agents
In src/lib/target-validation.ts around lines 145–148 (and also affecting
157–225), isLocalhostDomain and isPublicTarget miss FQDNs with a trailing dot
(e.g., "localhost." or "app.localhost.") and fail to detect IPv4‑mapped IPv6
literals (e.g., "::ffff:10.0.0.1"), and host:port forms can confuse IPv4 checks;
fix by normalizing input: trim trailing dot from domain names before matching
localhost and subdomains, strip a trailing ":port" when present (only for
IPv4-style host:port), detect and extract IPv4 from IPv4‑mapped IPv6 addresses
(map ::ffff:a.b.c.d to a.b.c.d) and then reuse the existing IPv4
private/loopback checks so these cases are properly classified as non-public.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/mcp/tools.ts (1)

75-81: Validation correctly implemented and consistently applied.

The target validation is properly placed before token retrieval across all measurement tools, preventing invalid requests early with clear error messages. The implementation correctly blocks private IPs, localhost, and link-local addresses as intended.

To reduce duplication, consider extracting the validation logic into a helper function:

/**
 * Validate that a target is a public endpoint
 * @throws Error if target is not public
 */
function validatePublicTarget(target: string): void {
	const validation = isPublicTarget(target);
	if (!validation.valid) {
		throw new Error(
			`Invalid target: ${validation.reason}. Globalping only supports public endpoints. Private IP addresses (RFC1918), localhost, and link-local addresses are not allowed.`,
		);
	}
}

Then replace each validation block with:

validatePublicTarget(target);

This would centralize the error message and make future updates easier, though the current implementation is perfectly acceptable.

Also applies to: 178-184, 302-308, 412-418, 528-534

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 127eae2 and ff4f90f.

📒 Files selected for processing (1)
  • src/mcp/tools.ts (11 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{vue,ts,js}

⚙️ CodeRabbit configuration file

We use Nuxt with auto imports enabled. Don't warn about missing imports.

Files:

  • src/mcp/tools.ts
🧬 Code graph analysis (1)
src/mcp/tools.ts (1)
src/lib/target-validation.ts (1)
  • isPublicTarget (157-226)
🔍 Remote MCP DeepWiki

Summary of additional repo context relevant to reviewing PR #28 ("Dont allow private ips"):

  • Project architecture and where tool/validation integrates

    • Cloudflare Worker + Durable Object MCP agent (GlobalpingMCP) registers tools and routes /mcp and /sse; tools call Globalping API via src/api/client.ts.
  • Where to check integrations/usage of isPublicTarget

    • Tool registration/handlers live under src/mcp and are invoked by GlobalpingMCP.init(); API client (src/api/client.ts) and tool wrappers call create/runMeasurement. PR changes should be reviewed in src/mcp/tools.ts and GlobalpingMCP tool registration paths.
  • OAuth / token flow does validation before routing to Durable Object

    • Main fetch handler detects API token vs OAuth and routes to handleAPITokenRequest or OAuthProvider → handleMcpRequest → GlobalpingMCP. New target validation in tools.ts will reject targets before token use. Review interaction with isAPITokenRequest/handleAPITokenRequest.
  • URL / redirect validation and existing helpers

    • Repo contains URL validation utilities (isTrustedRedirectUri, isLocalhost, isDeepLink, exception hosts) and tests — relevant for consistency with new target-localhost/domain checks.
  • Tests and test strategy

    • Unit and integration test setup: Vitest unit tests for lib functions and workers integration tests. New unit tests were added (test/unit/lib/target-validation.test.ts) — run unit suite and Workers integration suite per repo's test scripts.
  • Config and deployment implications

    • Worker routes, KV (OAUTH_KV) and Durable Object binding names are defined in wrangler.jsonc; ensure no runtime binding/name changes required by PR.,

Files/areas to focus review on (quick checklist):

  • src/lib/target-validation.ts and exported API surface (ensure edge cases, bracketed IPv6, trimming, clear reasons).
  • src/mcp/tools.ts modifications where isPublicTarget is used — confirm errors returned before any Globalping API calls and messages match docs/UI.
  • test/unit/lib/target-validation.test.ts — run locally; verify coverage for RFC1918 ranges, IPv6 ULA/link-local, localhost variants, bracket handling, and reason strings.
  • Integration points: handleAPITokenRequest and handleMcpRequest flow (src/index.ts) to ensure props/token logic unaffected.
  • URL validation/exception host logic overlap to avoid conflicting trust rules (src/lib/url-validation.ts area covered in docs).
  • CI/test configuration (vitest unit + workers integration) to ensure added tests will run in pipeline.

Sources used:

  • Repo documentation and pages (architecture, MCP server, API client, auth, tooling, testing) — DeepWiki read of repository docs and contents.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: globalping-mcp-server
🔇 Additional comments (1)
src/mcp/tools.ts (1)

9-9: LGTM!

The import is correctly placed and the validation function is properly integrated into all measurement tools.

@jimaek jimaek merged commit 2ff3891 into master Nov 29, 2025
4 checks passed
@jimaek jimaek deleted the privateips branch November 29, 2025 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants