[BUGFIX] t3lib_div::cmpIPv6() fails to compare
authorStefan Neufeind <typo3.neufeind@speedpartner.de>
Sat, 4 Jun 2011 18:25:37 +0000 (20:25 +0200)
committerXavier Perseguers <typo3@perseguers.ch>
Tue, 12 Jul 2011 11:19:52 +0000 (13:19 +0200)
cmpIPv6 makes wrong assumptions about the output of IPv6Hex2Bin().
Also currently only netmasks of /48, /64 and /128 are supported.

Added a cleanup for an E_NOTICE on exploding the bitmask.
Replaced IPv6Hex2Bin() with php function (same output, just quicker).
Added testcases.

Change-Id: I816db2b9ce42d13f61ecd7f87406730c424de52f
Resolves: #27210
Releases: 4.6, 4.5
Reviewed-on: http://review.typo3.org/2519
Reviewed-by: Stefan Neufeind
Tested-by: Stefan Neufeind
Reviewed-by: Xavier Perseguers
Tested-by: Xavier Perseguers
t3lib/class.t3lib_div.php
tests/t3lib/class.t3lib_divTest.php

index 6a8c2a6..7ebbae8 100644 (file)
@@ -471,29 +471,38 @@ final class t3lib_div {
 
                $values = self::trimExplode(',', $list, 1);
                foreach ($values as $test) {
-                       list($test, $mask) = explode('/', $test);
+                       $testList = explode('/', $test);
+                       if (count($testList) == 2) {
+                               list($test, $mask) = $testList;
+                       } else {
+                               $mask = FALSE;
+                       }
+
                        if (self::validIPv6($test)) {
                                $test = self::normalizeIPv6($test);
-                               if (intval($mask)) {
-                                       switch ($mask) { // test on /48 /64
-                                               case '48':
-                                                       $testBin = substr(self::IPv6Hex2Bin($test), 0, 48);
-                                                       $baseIPBin = substr(self::IPv6Hex2Bin($baseIP), 0, 48);
-                                                       $success = strcmp($testBin, $baseIPBin) == 0 ? TRUE : FALSE;
-                                                       break;
-                                               case '64':
-                                                       $testBin = substr(self::IPv6Hex2Bin($test), 0, 64);
-                                                       $baseIPBin = substr(self::IPv6Hex2Bin($baseIP), 0, 64);
-                                                       $success = strcmp($testBin, $baseIPBin) == 0 ? TRUE : FALSE;
-                                                       break;
-                                               default:
-                                                       $success = FALSE;
-                                       }
+                               $maskInt = intval($mask) ? intval($mask) : 128;
+                               if ($mask === '0') { // special case; /0 is an allowed mask - equals a wildcard
+                                       $success = TRUE;
+                               } elseif ($maskInt == 128) {
+                                       $success = ($test === $baseIP);
                                } else {
-                                       if (self::validIPv6($test)) { // test on full ip address 128 bits
-                                               $testBin = self::IPv6Hex2Bin($test);
-                                               $baseIPBin = self::IPv6Hex2Bin($baseIP);
-                                               $success = strcmp($testBin, $baseIPBin) == 0 ? TRUE : FALSE;
+                                       $testBin = self::IPv6Hex2Bin($test);
+                                       $baseIPBin = self::IPv6Hex2Bin($baseIP);
+                                       $success = TRUE;
+
+                                       // modulo is 0 if this is a 8-bit-boundary
+                                       $maskIntModulo = $maskInt % 8;
+                                       $numFullCharactersUntilBoundary = intval($maskInt / 8);
+
+                                       if (substr($testBin, 0, $numFullCharactersUntilBoundary) !== substr($baseIPBin, 0, $numFullCharactersUntilBoundary)) {
+                                               $success = FALSE;
+                                       } elseif ($maskIntModulo > 0) {
+                                               // if not an 8-bit-boundary, check bits of last character
+                                               $testLastBits = str_pad(decbin(ord(substr($testBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
+                                               $baseIPLastBits = str_pad(decbin(ord(substr($baseIPBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
+                                               if (strncmp($testLastBits, $baseIPLastBits, $maskIntModulo) != 0) {
+                                                       $success = FALSE;
+                                               }
                                        }
                                }
                        }
@@ -505,17 +514,19 @@ final class t3lib_div {
        }
 
        /**
-        * Convert an IPv6 address to its binary representation
+        * Transform a regular IPv6 address from hex-representation into binary
         *
-        * @param string $hex IPv6 address in hexadecimal form
-        * @return integer
+        * @param string $hex IPv6 address in hex-presentation
+        * @return string Binary representation (16 characters, 128 characters)
+        * @see normalizeIPv6()
         */
        public static function IPv6Hex2Bin($hex) {
-               $bin = '';
-               $hex = str_replace(':', '', $hex); // Replace colon to nothing
-               for ($i = 0; $i < strlen($hex); $i = $i + 2) {
-                       $bin .= chr(hexdec(substr($hex, $i, 2)));
+               // normalized representation has 39 characters (0000:0000:0000:0000:0000:0000:0000:0000)
+               if (strlen($hex) < 39) {
+                       $hex = self::normalizeIPv6($hex);
                }
+               $hex = str_replace(':', '', $hex); // Replace colon to nothing
+               $bin = pack("H*" , $hex);
                return $bin;
        }
 
index dc96f10..e87ee2a 100644 (file)
@@ -182,6 +182,120 @@ class t3lib_divTest extends tx_phpunit_testcase {
                $this->assertEquals($resultFilePermissions, '0777');
        }
 
+       ///////////////////////////
+       // Tests concerning cmpIPv6
+       ///////////////////////////
+
+       /**
+        * Data provider for cmpIPv6ReturnsTrueForMatchingAddress
+        *
+        * @return array Data sets
+        */
+       public static function cmpIPv6DataProviderMatching() {
+               return array(
+                       'empty address' => array('::', '::'),
+                       'empty with netmask in list' => array('::', '::/0'),
+                       'empty with netmask 0 and host-bits set in list' => array('::', '::123/0'),
+                       'localhost' => array('::1', '::1'),
+                       'localhost with leading zero blocks' => array('::1', '0:0::1'),
+                       'host with submask /128' => array('::1', '0:0::1/128'),
+                       '/16 subnet' => array('1234::1', '1234:5678::/16'),
+                       '/126 subnet' => array('1234:5678::3', '1234:5678::/126'),
+                       '/126 subnet with host-bits in list set' => array('1234:5678::3', '1234:5678::2/126'),
+                       'list with IPv4/IPv6 addresses' => array('1234:5678::3', '::1, 127.0.0.1, 1234:5678::/126, 192.168.1.1'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider cmpIPv6DataProviderMatching
+        */
+       public function cmpIPv6ReturnsTrueForMatchingAddress($ip, $list) {
+               $this->assertTrue(t3lib_div::cmpIPv6($ip, $list));
+       }
+
+       /**
+        * Data provider for cmpIPv6ReturnsFalseForNotMatchingAddress
+        *
+        * @return array Data sets
+        */
+       public static function cmpIPv6DataProviderNotMatching() {
+               return array(
+                       'empty against localhost' => array('::', '::1'),
+                       'empty against localhost with /128 netmask' => array('::', '::1/128'),
+                       'localhost against different host' => array('::1', '::2'),
+                       'localhost against host with prior bits set' => array('::1', '::1:1'),
+                       'host against different /17 subnet' => array('1234::1', '1234:f678::/17'),
+                       'host against different /127 subnet' => array('1234:5678::3', '1234:5678::/127'),
+                       'host against IPv4 address list' => array('1234:5678::3', '127.0.0.1, 192.168.1.1'),
+                       'host against mixed list with IPv6 host in different subnet' => array('1234:5678::3', '::1, 1234:5678::/127'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider cmpIPv6DataProviderNotMatching
+        */
+       public function cmpIPv6ReturnsFalseForNotMatchingAddress($ip, $list) {
+               $this->assertFalse(t3lib_div::cmpIPv6($ip, $list));
+       }
+
+       ///////////////////////////////
+       // Tests concerning IPv6Hex2Bin
+       ///////////////////////////////
+
+       /**
+        * Data provider for IPv6Hex2BinReturnsCorrectBinaryHosts
+        *
+        * @return array Data sets
+        */
+       public static function IPv6Hex2BinDataProviderCorrectlyConverted() {
+               return array(
+                       'empty 1' => array('::', str_pad('', 16, "\x00")),
+                       'empty 2, already normalized' => array('0000:0000:0000:0000:0000:0000:0000:0000', str_pad('', 16, "\x00")),
+                       'empty 3, already normalized' => array('0102:0304:0000:0000:0000:0000:0506:0078', "\x01\x02\x03\x04" . str_pad('', 8, "\x00") . "\x05\x06\x00\x78"),
+                       'expansion in middle 1' => array('1::2', "\x00\x01" . str_pad('', 12, "\x00") . "\x00\x02"),
+                       'expansion in middle 2' => array('beef::fefa', "\xbe\xef" . str_pad('', 12, "\x00") . "\xfe\xfa"),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider IPv6Hex2BinDataProviderCorrectlyConverted
+        */
+       public function IPv6Hex2BinReturnsCorrectBinaryHosts($inputIP, $binary) {
+               $this->assertTrue(t3lib_div::IPv6Hex2Bin($inputIP) === $binary);
+       }
+
+       /////////////////////////////////
+       // Tests concerning normalizeIPv6
+       /////////////////////////////////
+
+       /**
+        * Data provider for normalizeIPv6ReturnsCorrectlyNormalizedFormat
+        *
+        * @return array Data sets
+        */
+       public static function normalizeIPv6DataProviderCorrectlyNormalized() {
+               return array(
+                       'empty' => array('::', '0000:0000:0000:0000:0000:0000:0000:0000'),
+                       'localhost' => array('::1', '0000:0000:0000:0000:0000:0000:0000:0001'),
+                       'some address on right side' => array('::F0F', '0000:0000:0000:0000:0000:0000:0000:0F0F'),
+                       'expansion in middle 1' => array('1::2', '0001:0000:0000:0000:0000:0000:0000:0002'),
+                       'expansion in middle 2' => array('1:2::3', '0001:0002:0000:0000:0000:0000:0000:0003'),
+                       'expansion in middle 3' => array('1::2:3', '0001:0000:0000:0000:0000:0000:0002:0003'),
+                       'expansion in middle 4' => array('1:2::3:4:5', '0001:0002:0000:0000:0000:0003:0004:0005'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider normalizeIPv6DataProviderCorrectlyNormalized
+        */
+       public function normalizeIPv6ReturnsCorrectlyNormalizedFormat($inputIP, $normalized) {
+               $this->assertTrue(t3lib_div::normalizeIPv6($inputIP) === $normalized);
+       }
+
        ///////////////////////////////
        // Tests concerning validIP
        ///////////////////////////////