diff --git a/composer.json b/composer.json index b5550de..5cd4f06 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ } ], "require": { + "php": ">=7.1", "indieweb/link-rel-parser": "0.1.*", "mf2/mf2": ">=0.3.2" }, @@ -25,7 +26,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^10 || ^11" + "phpunit/phpunit": "^9.6 || ^10 || ^11" }, "scripts": { "test": "phpunit" diff --git a/src/p3k/HTTP/Guard.php b/src/p3k/HTTP/Guard.php index cb7e05c..38dbfff 100644 --- a/src/p3k/HTTP/Guard.php +++ b/src/p3k/HTTP/Guard.php @@ -14,6 +14,18 @@ class Guard { // read as an IP address. Refuse them rather than guess which one. const NUMERIC_HOST = '/^(0x[0-9a-f]*|[0-9]+)(\.(0x[0-9a-f]*|[0-9]+))*\.?$/i'; + // Addresses that are not on the public internet: the IANA IPv4 and IPv6 + // special-purpose registries, plus multicast. Listed here rather than + // left to FILTER_FLAG_GLOBAL_RANGE, which needs PHP 8.2, so that every + // PHP version refuses the same addresses. + const NON_PUBLIC = [ + '0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8', '169.254.0.0/16', + '172.16.0.0/12', '192.0.0.0/24', '192.0.2.0/24', '192.88.99.0/24', '192.168.0.0/16', + '198.18.0.0/15', '198.51.100.0/24', '203.0.113.0/24', '224.0.0.0/4', '240.0.0.0/4', + '::/128', '::1/128', '::ffff:0:0/96', '64:ff9b:1::/48', '100::/64', '2001::/23', + '2001:db8::/32', '3fff::/20', '5f00::/16', 'fc00::/7', 'fe80::/10', 'fec0::/10', 'ff00::/8', + ]; + private $_allow_hosts = []; private $_allow_cidrs = []; private $_resolver; @@ -90,9 +102,14 @@ class Guard { /** Whether an address is on the public internet, looking inside IPv6 forms that carry an IPv4 address. */ public static function is_public($address) { - if(!filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE)) + if(!filter_var($address, FILTER_VALIDATE_IP)) return false; + foreach(self::NON_PUBLIC as $cidr) { + if(self::in_cidr($address, $cidr)) + return false; + } + $embedded = self::_embedded_ipv4($address); if($embedded !== null) return self::is_public($embedded); @@ -125,13 +142,13 @@ class Guard { return self::is_public($address); } - // NAT64 (64:ff9b::/96, 64:ff9b:1::/48) and 6to4 (2002::/16) addresses - // reach an IPv4 address, which must be public too. + // NAT64 (64:ff9b::/96) and 6to4 (2002::/16) addresses reach an IPv4 + // address, which must be public too. private static function _embedded_ipv4($address) { $bin = @inet_pton($address); if($bin === false || strlen($bin) !== 16) return null; - if(self::in_cidr($address, '64:ff9b::/96') || self::in_cidr($address, '64:ff9b:1::/48')) + if(self::in_cidr($address, '64:ff9b::/96')) return inet_ntop(substr($bin, 12, 4)); if(self::in_cidr($address, '2002::/16')) return inet_ntop(substr($bin, 2, 4)); diff --git a/tests/GuardTest.php b/tests/GuardTest.php index c953cd7..efed7c7 100644 --- a/tests/GuardTest.php +++ b/tests/GuardTest.php @@ -11,7 +11,7 @@ class GuardTest extends TestCase { } public static function publicAddresses() { - return [['8.8.8.8'], ['93.184.216.34'], ['2606:4700::1'], ['2001:4860:4860::8888']]; + return [['8.8.8.8'], ['93.184.216.34'], ['100.128.0.1'], ['198.20.0.1'], ['223.255.255.255'], ['2606:4700::1'], ['2001:4860:4860::8888'], ['64:ff9b::808:808'], ['2002:808:808::']]; } public static function nonPublicAddresses() { @@ -19,14 +19,19 @@ class GuardTest extends TestCase { ['127.0.0.1'], ['127.8.9.10'], ['10.11.11.80'], ['172.16.0.1'], ['192.168.1.1'], ['169.254.169.254'], ['100.64.0.1'], ['0.0.0.0'], ['::'], ['::1'], ['fe80::1'], ['fc00::1'], ['fd12:3456::1'], ['::ffff:127.0.0.1'], ['::ffff:10.0.0.1'], ['64:ff9b::7f00:1'], ['64:ff9b::a00:1'], ['2002:7f00:1::1'], ['2002:a9fe:a9fe::'], + ['192.0.0.8'], ['192.0.2.1'], ['198.18.0.1'], ['198.19.255.255'], ['198.51.100.7'], ['203.0.113.5'], ['224.0.0.1'], + ['239.255.255.250'], ['255.255.255.255'], ['100.127.255.255'], ['::ffff:8.8.8.8'], ['64:ff9b:1::1'], ['100::1'], + ['2001::1'], ['2001:db8::1'], ['3fff::1'], ['fec0::1'], ['ff02::1'], ['not an address'], [''], ]; } + /** @dataProvider publicAddresses */ #[\PHPUnit\Framework\Attributes\DataProvider('publicAddresses')] public function testPublicAddresses($address) { $this->assertTrue(Guard::is_public($address)); } + /** @dataProvider nonPublicAddresses */ #[\PHPUnit\Framework\Attributes\DataProvider('nonPublicAddresses')] public function testNonPublicAddresses($address) { $this->assertFalse(Guard::is_public($address)); @@ -42,6 +47,7 @@ class GuardTest extends TestCase { ]; } + /** @dataProvider blockedUrls */ #[\PHPUnit\Framework\Attributes\DataProvider('blockedUrls')] public function testBlockedUrls($url) { $result = self::guard(['localhost.example' => ['127.0.0.1'], 'mixed.example' => ['93.184.216.34', '10.0.0.1']])->check($url);