Skip to content

Commit 785e3a7

Browse files
committed
use ipaddr
1 parent c85350f commit 785e3a7

File tree

4 files changed

+42
-81
lines changed

4 files changed

+42
-81
lines changed

apps/sim/lib/core/security/input-validation.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -906,13 +906,13 @@ describe('validateExternalUrl', () => {
906906
it.concurrent('should reject 127.0.0.1', () => {
907907
const result = validateExternalUrl('https://127.0.0.1/api')
908908
expect(result.isValid).toBe(false)
909-
expect(result.error).toContain('localhost')
909+
expect(result.error).toContain('private IP')
910910
})
911911

912912
it.concurrent('should reject 0.0.0.0', () => {
913913
const result = validateExternalUrl('https://0.0.0.0/api')
914914
expect(result.isValid).toBe(false)
915-
expect(result.error).toContain('localhost')
915+
expect(result.error).toContain('private IP')
916916
})
917917
})
918918

apps/sim/lib/core/security/input-validation.ts

Lines changed: 35 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import dns from 'dns/promises'
22
import http from 'http'
33
import https from 'https'
44
import { createLogger } from '@sim/logger'
5+
import * as ipaddr from 'ipaddr.js'
56

67
const logger = createLogger('InputValidation')
78

@@ -404,42 +405,20 @@ export function validateHostname(
404405
}
405406
}
406407

407-
// Import the blocked IP ranges from url-validation
408-
const BLOCKED_IP_RANGES = [
409-
// Private IPv4 ranges (RFC 1918)
410-
/^10\./,
411-
/^172\.(1[6-9]|2[0-9]|3[01])\./,
412-
/^192\.168\./,
413-
414-
// Loopback addresses
415-
/^127\./,
416-
/^localhost$/i,
417-
418-
// Link-local addresses (RFC 3927)
419-
/^169\.254\./,
420-
421-
// Cloud metadata endpoints
422-
/^169\.254\.169\.254$/,
423-
424-
// Broadcast and other reserved ranges
425-
/^0\./,
426-
/^224\./,
427-
/^240\./,
428-
/^255\./,
429-
430-
// IPv6 loopback and link-local
431-
/^::1$/,
432-
/^fe80:/i,
433-
/^::ffff:127\./i,
434-
/^::ffff:10\./i,
435-
/^::ffff:172\.(1[6-9]|2[0-9]|3[01])\./i,
436-
/^::ffff:192\.168\./i,
437-
]
438-
439408
const lowerHostname = hostname.toLowerCase()
440409

441-
for (const pattern of BLOCKED_IP_RANGES) {
442-
if (pattern.test(lowerHostname)) {
410+
// Block localhost
411+
if (lowerHostname === 'localhost') {
412+
logger.warn('Hostname is localhost', { paramName })
413+
return {
414+
isValid: false,
415+
error: `${paramName} cannot be a private IP address or localhost`,
416+
}
417+
}
418+
419+
// Use ipaddr.js to check if hostname is an IP and if it's private/reserved
420+
if (ipaddr.isValid(lowerHostname)) {
421+
if (isPrivateOrReservedIP(lowerHostname)) {
443422
logger.warn('Hostname matches blocked IP range', {
444423
paramName,
445424
hostname: hostname.substring(0, 100),
@@ -712,33 +691,17 @@ export function validateExternalUrl(
712691
// Block private IP ranges and localhost
713692
const hostname = parsedUrl.hostname.toLowerCase()
714693

715-
// Block localhost variations
716-
if (
717-
hostname === 'localhost' ||
718-
hostname === '127.0.0.1' ||
719-
hostname === '::1' ||
720-
hostname.startsWith('127.') ||
721-
hostname === '0.0.0.0'
722-
) {
694+
// Block localhost
695+
if (hostname === 'localhost') {
723696
return {
724697
isValid: false,
725698
error: `${paramName} cannot point to localhost`,
726699
}
727700
}
728701

729-
// Block private IP ranges
730-
const privateIpPatterns = [
731-
/^10\./,
732-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
733-
/^192\.168\./,
734-
/^169\.254\./, // Link-local
735-
/^fe80:/i, // IPv6 link-local
736-
/^fc00:/i, // IPv6 unique local
737-
/^fd00:/i, // IPv6 unique local
738-
]
739-
740-
for (const pattern of privateIpPatterns) {
741-
if (pattern.test(hostname)) {
702+
// Use ipaddr.js to check if hostname is an IP and if it's private/reserved
703+
if (ipaddr.isValid(hostname)) {
704+
if (isPrivateOrReservedIP(hostname)) {
742705
return {
743706
isValid: false,
744707
error: `${paramName} cannot point to private IP addresses`,
@@ -793,30 +756,25 @@ export function validateProxyUrl(
793756

794757
/**
795758
* Checks if an IP address is private or reserved (not routable on the public internet)
759+
* Uses ipaddr.js for robust handling of all IP formats including:
760+
* - Octal notation (0177.0.0.1)
761+
* - Hex notation (0x7f000001)
762+
* - IPv4-mapped IPv6 (::ffff:127.0.0.1)
763+
* - Various edge cases that regex patterns miss
796764
*/
797765
function isPrivateOrReservedIP(ip: string): boolean {
798-
const patterns = [
799-
/^127\./, // Loopback
800-
/^10\./, // Private Class A
801-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private Class B
802-
/^192\.168\./, // Private Class C
803-
/^169\.254\./, // Link-local
804-
/^0\./, // Current network
805-
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./, // Carrier-grade NAT
806-
/^192\.0\.0\./, // IETF Protocol Assignments
807-
/^192\.0\.2\./, // TEST-NET-1
808-
/^198\.51\.100\./, // TEST-NET-2
809-
/^203\.0\.113\./, // TEST-NET-3
810-
/^224\./, // Multicast
811-
/^240\./, // Reserved
812-
/^255\./, // Broadcast
813-
/^::1$/, // IPv6 loopback
814-
/^fe80:/i, // IPv6 link-local
815-
/^fc00:/i, // IPv6 unique local
816-
/^fd00:/i, // IPv6 unique local
817-
/^::ffff:(127\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.|169\.254\.)/i, // IPv4-mapped IPv6
818-
]
819-
return patterns.some((pattern) => pattern.test(ip))
766+
try {
767+
if (!ipaddr.isValid(ip)) {
768+
return true
769+
}
770+
771+
const addr = ipaddr.process(ip)
772+
const range = addr.range()
773+
774+
return range !== 'unicast'
775+
} catch {
776+
return true
777+
}
820778
}
821779

822780
/**

apps/sim/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"imapflow": "1.2.4",
109109
"input-otp": "^1.4.2",
110110
"ioredis": "^5.6.0",
111+
"ipaddr.js": "2.3.0",
111112
"isolated-vm": "6.0.2",
112113
"jose": "6.0.11",
113114
"js-tiktoken": "1.0.21",

bun.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)