@@ -2,6 +2,7 @@ import dns from 'dns/promises'
22import http from 'http'
33import https from 'https'
44import { createLogger } from '@sim/logger'
5+ import * as ipaddr from 'ipaddr.js'
56
67const 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- / ^ 1 0 \. / ,
411- / ^ 1 7 2 \. ( 1 [ 6 - 9 ] | 2 [ 0 - 9 ] | 3 [ 0 1 ] ) \. / ,
412- / ^ 1 9 2 \. 1 6 8 \. / ,
413-
414- // Loopback addresses
415- / ^ 1 2 7 \. / ,
416- / ^ l o c a l h o s t $ / i,
417-
418- // Link-local addresses (RFC 3927)
419- / ^ 1 6 9 \. 2 5 4 \. / ,
420-
421- // Cloud metadata endpoints
422- / ^ 1 6 9 \. 2 5 4 \. 1 6 9 \. 2 5 4 $ / ,
423-
424- // Broadcast and other reserved ranges
425- / ^ 0 \. / ,
426- / ^ 2 2 4 \. / ,
427- / ^ 2 4 0 \. / ,
428- / ^ 2 5 5 \. / ,
429-
430- // IPv6 loopback and link-local
431- / ^ : : 1 $ / ,
432- / ^ f e 8 0 : / i,
433- / ^ : : f f f f : 1 2 7 \. / i,
434- / ^ : : f f f f : 1 0 \. / i,
435- / ^ : : f f f f : 1 7 2 \. ( 1 [ 6 - 9 ] | 2 [ 0 - 9 ] | 3 [ 0 1 ] ) \. / i,
436- / ^ : : f f f f : 1 9 2 \. 1 6 8 \. / 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- / ^ 1 0 \. / ,
732- / ^ 1 7 2 \. ( 1 [ 6 - 9 ] | 2 [ 0 - 9 ] | 3 [ 0 - 1 ] ) \. / ,
733- / ^ 1 9 2 \. 1 6 8 \. / ,
734- / ^ 1 6 9 \. 2 5 4 \. / , // Link-local
735- / ^ f e 8 0 : / i, // IPv6 link-local
736- / ^ f c 0 0 : / i, // IPv6 unique local
737- / ^ f d 0 0 : / 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 */
797765function isPrivateOrReservedIP ( ip : string ) : boolean {
798- const patterns = [
799- / ^ 1 2 7 \. / , // Loopback
800- / ^ 1 0 \. / , // Private Class A
801- / ^ 1 7 2 \. ( 1 [ 6 - 9 ] | 2 [ 0 - 9 ] | 3 [ 0 - 1 ] ) \. / , // Private Class B
802- / ^ 1 9 2 \. 1 6 8 \. / , // Private Class C
803- / ^ 1 6 9 \. 2 5 4 \. / , // Link-local
804- / ^ 0 \. / , // Current network
805- / ^ 1 0 0 \. ( 6 [ 4 - 9 ] | [ 7 - 9 ] [ 0 - 9 ] | 1 [ 0 - 1 ] [ 0 - 9 ] | 1 2 [ 0 - 7 ] ) \. / , // Carrier-grade NAT
806- / ^ 1 9 2 \. 0 \. 0 \. / , // IETF Protocol Assignments
807- / ^ 1 9 2 \. 0 \. 2 \. / , // TEST-NET-1
808- / ^ 1 9 8 \. 5 1 \. 1 0 0 \. / , // TEST-NET-2
809- / ^ 2 0 3 \. 0 \. 1 1 3 \. / , // TEST-NET-3
810- / ^ 2 2 4 \. / , // Multicast
811- / ^ 2 4 0 \. / , // Reserved
812- / ^ 2 5 5 \. / , // Broadcast
813- / ^ : : 1 $ / , // IPv6 loopback
814- / ^ f e 8 0 : / i, // IPv6 link-local
815- / ^ f c 0 0 : / i, // IPv6 unique local
816- / ^ f d 0 0 : / i, // IPv6 unique local
817- / ^ : : f f f f : ( 1 2 7 \. | 1 0 \. | 1 7 2 \. ( 1 [ 6 - 9 ] | 2 [ 0 - 9 ] | 3 [ 0 - 1 ] ) \. | 1 9 2 \. 1 6 8 \. | 1 6 9 \. 2 5 4 \. ) / 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/**
0 commit comments