-
Notifications
You must be signed in to change notification settings - Fork 5
Dont allow private ips #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| export * from "./crypto"; | ||
| export * from "./url-validation"; | ||
| export * from "./target-validation"; | ||
| export * from "./security"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| /** | ||
| * Target validation utilities for Globalping measurements | ||
| * Ensures only public endpoints are tested (Globalping API requirement) | ||
| */ | ||
|
|
||
| /** | ||
| * Check if an IPv4 address is in a private range (RFC1918) | ||
| * Private ranges: | ||
| * - 10.0.0.0/8 (10.0.0.0 - 10.255.255.255) | ||
| * - 172.16.0.0/12 (172.16.0.0 - 172.31.255.255) | ||
| * - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255) | ||
| * | ||
| * @param ip The IPv4 address to check | ||
| * @returns True if the IP is in a private range | ||
| */ | ||
| export function isPrivateIPv4(ip: string): boolean { | ||
| // Remove any IPv6 brackets if present | ||
| const cleanIp = ip.replace(/^\[|\]$/g, ""); | ||
|
|
||
| // Parse IPv4 address into octets | ||
| const parts = cleanIp.split("."); | ||
| if (parts.length !== 4) { | ||
| return false; | ||
| } | ||
|
|
||
| const octets = parts.map((part) => Number.parseInt(part, 10)); | ||
|
|
||
| // Validate all octets are numbers 0-255 | ||
| if (octets.some((octet) => Number.isNaN(octet) || octet < 0 || octet > 255)) { | ||
| return false; | ||
| } | ||
|
|
||
| const [first, second] = octets; | ||
|
|
||
| // 10.0.0.0/8 | ||
| if (first === 10) { | ||
| return true; | ||
| } | ||
|
|
||
| // 172.16.0.0/12 | ||
| if (first === 172 && second >= 16 && second <= 31) { | ||
| return true; | ||
| } | ||
|
|
||
| // 192.168.0.0/16 | ||
| if (first === 192 && second === 168) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Check if an IPv6 address is in a private range | ||
| * Private ranges: | ||
| * - fc00::/7 (Unique Local Addresses) | ||
| * - fe80::/10 (Link-local addresses) | ||
| * | ||
| * @param ip The IPv6 address to check | ||
| * @returns True if the IP is in a private range | ||
| */ | ||
| export function isPrivateIPv6(ip: string): boolean { | ||
| // Remove brackets if present | ||
| const cleanIp = ip.replace(/^\[|\]$/g, "").toLowerCase(); | ||
|
|
||
| // fc00::/7 - Unique Local Addresses (fc00:: to fdff::) | ||
| if (cleanIp.startsWith("fc") || cleanIp.startsWith("fd")) { | ||
| return true; | ||
| } | ||
|
|
||
| // fe80::/10 - Link-local addresses | ||
| if (cleanIp.startsWith("fe8") || cleanIp.startsWith("fe9") || cleanIp.startsWith("fea") || cleanIp.startsWith("feb")) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Check if an IPv4 address is a loopback address | ||
| * Loopback range: 127.0.0.0/8 | ||
| * | ||
| * @param ip The IPv4 address to check | ||
| * @returns True if the IP is a loopback address | ||
| */ | ||
| export function isLoopbackIPv4(ip: string): boolean { | ||
| const cleanIp = ip.replace(/^\[|\]$/g, ""); | ||
| const parts = cleanIp.split("."); | ||
|
|
||
| if (parts.length !== 4) { | ||
| return false; | ||
| } | ||
|
|
||
| const first = Number.parseInt(parts[0], 10); | ||
| return first === 127; | ||
| } | ||
|
|
||
| /** | ||
| * Check if an IPv6 address is a loopback address | ||
| * Loopback: ::1 | ||
| * | ||
| * @param ip The IPv6 address to check | ||
| * @returns True if the IP is a loopback address | ||
| */ | ||
| export function isLoopbackIPv6(ip: string): boolean { | ||
| const cleanIp = ip.replace(/^\[|\]$/g, "").toLowerCase(); | ||
|
|
||
| // ::1 is the only IPv6 loopback | ||
| return cleanIp === "::1" || cleanIp === "0:0:0:0:0:0:0:1" || cleanIp === "0000:0000:0000:0000:0000:0000:0000:0001"; | ||
| } | ||
|
|
||
| /** | ||
| * Check if an IPv4 address is a link-local address (APIPA) | ||
| * Link-local range: 169.254.0.0/16 | ||
| * | ||
| * @param ip The IPv4 address to check | ||
| * @returns True if the IP is a link-local address | ||
| */ | ||
| export function isLinkLocalIPv4(ip: string): boolean { | ||
| const cleanIp = ip.replace(/^\[|\]$/g, ""); | ||
| const parts = cleanIp.split("."); | ||
|
|
||
| if (parts.length !== 4) { | ||
| return false; | ||
| } | ||
|
|
||
| const octets = parts.map((part) => Number.parseInt(part, 10)); | ||
|
|
||
| // Validate octets | ||
| if (octets.some((octet) => Number.isNaN(octet) || octet < 0 || octet > 255)) { | ||
| return false; | ||
| } | ||
|
|
||
| // 169.254.0.0/16 | ||
| return octets[0] === 169 && octets[1] === 254; | ||
| } | ||
|
|
||
| /** | ||
| * Check if a target is a localhost domain | ||
| * Matches: localhost, *.localhost | ||
| * | ||
| * @param target The target to check | ||
| * @returns True if the target is a localhost domain | ||
| */ | ||
| export function isLocalhostDomain(target: string): boolean { | ||
| const lower = target.toLowerCase(); | ||
| return lower === "localhost" || lower.endsWith(".localhost"); | ||
| } | ||
|
|
||
| /** | ||
| * Validate if a target is a public endpoint | ||
| * Globalping only supports public endpoints - no private IPs, localhost, or link-local addresses | ||
| * | ||
| * @param target The target domain or IP address to validate | ||
| * @returns Validation result with reason if invalid | ||
| */ | ||
| export function isPublicTarget(target: string): { valid: boolean; reason?: string } { | ||
| if (!target || target.trim() === "") { | ||
| return { valid: false, reason: "Target cannot be empty" }; | ||
| } | ||
|
|
||
| const cleanTarget = target.trim(); | ||
|
|
||
| // Check for localhost domain patterns | ||
| if (isLocalhostDomain(cleanTarget)) { | ||
| return { | ||
| valid: false, | ||
| reason: `'${cleanTarget}' is a localhost domain`, | ||
| }; | ||
| } | ||
|
|
||
| // 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)) { | ||
| // Check IPv4 loopback | ||
| if (isLoopbackIPv4(ipToCheck)) { | ||
| return { | ||
| valid: false, | ||
| reason: `${ipToCheck} is a loopback address (127.0.0.0/8)`, | ||
| }; | ||
| } | ||
|
|
||
| // Check IPv4 private ranges | ||
| if (isPrivateIPv4(ipToCheck)) { | ||
| return { | ||
| valid: false, | ||
| reason: `${ipToCheck} is a private IPv4 address (RFC1918)`, | ||
| }; | ||
| } | ||
|
|
||
| // Check IPv4 link-local | ||
| if (isLinkLocalIPv4(ipToCheck)) { | ||
| return { | ||
| valid: false, | ||
| reason: `${ipToCheck} is a link-local address (169.254.0.0/16)`, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| // Check if it looks like an IPv6 address (contains colons) | ||
| if (ipToCheck.includes(":")) { | ||
| // Check IPv6 loopback | ||
| if (isLoopbackIPv6(ipToCheck)) { | ||
| return { | ||
| valid: false, | ||
| reason: `${ipToCheck} is the IPv6 loopback address`, | ||
| }; | ||
| } | ||
|
|
||
| // Check IPv6 private ranges | ||
| if (isPrivateIPv6(ipToCheck)) { | ||
| return { | ||
| valid: false, | ||
| reason: `${ipToCheck} is a private IPv6 address`, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| // If it doesn't match any private/local patterns, assume it's public | ||
| // (could be a domain name or a public IP) | ||
| return { valid: true }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Localhost FQDNs and IPv4‑mapped IPv6 addresses currently bypass the “public target” guard
Two notable edge cases slip through
isPublicTargetright now:Localhost with a trailing root dot
Domains such as
"localhost."and"app.localhost."are standard FQDN representations of the localhost namespace, butisLocalhostDomainonly matches"localhost"and*.localhostwithout a trailing dot. These inputs are therefore treated as public and accepted.IPv4‑mapped IPv6 literals with private IPv4 parts
Addresses like
::ffff:10.0.0.1or0:0:0:0:0:ffff:192.168.1.1are IPv6 forms of private IPv4 addresses. The current logic only checks IPv6 ULA/link‑local prefixes (fc,fd,fe8–feb) 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:
And in
isPublicTarget, normalize IPv4‑mapped IPv6 into the IPv4 path so the existing private/loopback/link‑local checks apply:Separately, if
targetcan 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‑stylehost:portbefore running this logic to avoid that loophole as well.Also applies to: 157-225
🤖 Prompt for AI Agents