Skip to content

Commit 78fd918

Browse files
[HttpFoundation] Add optional $anonymizedBytesForV4 and $anonymizedBytesForV6 arguments to IpUtils::anonymize()
1 parent 99b25a1 commit 78fd918

File tree

3 files changed

+93
-6
lines changed

3 files changed

+93
-6
lines changed

src/Symfony/Component/HttpFoundation/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add optional `$requests` argument to `RequestStack::__construct()`
8+
* Add optional `$anonymizedBytesForV4` and `$anonymizedBytesForV6` arguments to `IpUtils::anonymize()`
89

910
7.1
1011
---

src/Symfony/Component/HttpFoundation/IpUtils.php

+28-6
Original file line numberDiff line numberDiff line change
@@ -178,25 +178,47 @@ public static function checkIp6(string $requestIp, string $ip): bool
178178
/**
179179
* Anonymizes an IP/IPv6.
180180
*
181-
* Removes the last byte for v4 and the last 8 bytes for v6 IPs
181+
* Removes the last bytes of IPv4 and IPv6 addresses (1 byte for IPv4 and 8 bytes for IPv6 by default).
182+
*
183+
* @param int<0, 4> $anonymizedBytesForV4
184+
* @param int<0, 16> $anonymizedBytesForV6
182185
*/
183-
public static function anonymize(string $ip): string
186+
public static function anonymize(string $ip/* , int $anonymizedBytesForV4 = 1, int $anonymizedBytesForV6 = 8 */): string
184187
{
188+
$anonymizedBytesForV4 = 1 < \func_num_args() ? func_get_arg(1) : 1;
189+
$anonymizedBytesForV6 = 2 < \func_num_args() ? func_get_arg(2) : 8;
190+
191+
if ($anonymizedBytesForV4 < 0 || $anonymizedBytesForV6 < 0) {
192+
throw new \InvalidArgumentException('Cannot anonymize less than 0 bytes.');
193+
}
194+
195+
if ($anonymizedBytesForV4 > 4 || $anonymizedBytesForV6 > 16) {
196+
throw new \InvalidArgumentException('Cannot anonymize more than 4 bytes for IPv4 and 16 bytes for IPv6.');
197+
}
198+
185199
$wrappedIPv6 = false;
186200
if (str_starts_with($ip, '[') && str_ends_with($ip, ']')) {
187201
$wrappedIPv6 = true;
188202
$ip = substr($ip, 1, -1);
189203
}
190204

205+
$mappedIpV4MaskGenerator = function (string $mask, int $bytesToAnonymize) {
206+
$mask .= str_repeat('ff', 4 - $bytesToAnonymize);
207+
$mask .= str_repeat('00', $bytesToAnonymize);
208+
209+
return '::'.implode(':', str_split($mask, 4));
210+
};
211+
191212
$packedAddress = inet_pton($ip);
192213
if (4 === \strlen($packedAddress)) {
193-
$mask = '255.255.255.0';
214+
$mask = rtrim(str_repeat('255.', 4 - $anonymizedBytesForV4).str_repeat('0.', $anonymizedBytesForV4), '.');
194215
} elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) {
195-
$mask = '::ffff:ffff:ff00';
216+
$mask = $mappedIpV4MaskGenerator('ffff', $anonymizedBytesForV4);
196217
} elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) {
197-
$mask = '::ffff:ff00';
218+
$mask = $mappedIpV4MaskGenerator('', $anonymizedBytesForV4);
198219
} else {
199-
$mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000';
220+
$mask = str_repeat('ff', 16 - $anonymizedBytesForV6).str_repeat('00', $anonymizedBytesForV6);
221+
$mask = implode(':', str_split($mask, 4));
200222
}
201223
$ip = inet_ntop($packedAddress & inet_pton($mask));
202224

src/Symfony/Component/HttpFoundation/Tests/IpUtilsTest.php

+64
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,70 @@ public static function anonymizedIpData()
150150
];
151151
}
152152

153+
/**
154+
* @dataProvider anonymizedIpDataWithBytes
155+
*/
156+
public function testAnonymizeWithBytes($ip, $expected, $bytesForV4, $bytesForV6)
157+
{
158+
$this->assertSame($expected, IpUtils::anonymize($ip, $bytesForV4, $bytesForV6));
159+
}
160+
161+
public static function anonymizedIpDataWithBytes(): array
162+
{
163+
return [
164+
['192.168.1.1', '192.168.0.0', 2, 8],
165+
['192.168.1.1', '192.0.0.0', 3, 8],
166+
['192.168.1.1', '0.0.0.0', 4, 8],
167+
['1.2.3.4', '1.2.3.0', 1, 8],
168+
['1.2.3.4', '1.2.3.4', 0, 8],
169+
['2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0:396e:4789:8e99:890f', 1, 0],
170+
['2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0:396e:4789::', 1, 4],
171+
['2a01:198:603:10:396e:4789:8e99:890f', '2a01:198:603:10:396e:4700::', 1, 5],
172+
['2a01:198:603:10:396e:4789:8e99:890f', '2a00::', 1, 15],
173+
['2a01:198:603:10:396e:4789:8e99:890f', '::', 1, 16],
174+
['::1', '::', 1, 1],
175+
['0:0:0:0:0:0:0:1', '::', 1, 1],
176+
['1:0:0:0:0:0:0:1', '1::', 1, 1],
177+
['0:0:603:50:396e:4789:8e99:0001', '0:0:603::', 1, 10],
178+
['[0:0:603:50:396e:4789:8e99:0001]', '[::603:50:396e:4789:8e00:0]', 1, 3],
179+
['[2a01:198::3]', '[2a01:198::]', 1, 2],
180+
['::ffff:123.234.235.236', '::ffff:123.234.235.0', 1, 8], // IPv4-mapped IPv6 addresses
181+
['::123.234.235.236', '::123.234.0.0', 2, 8], // deprecated IPv4-compatible IPv6 address
182+
];
183+
}
184+
185+
public function testAnonymizeV4WithNegativeBytes()
186+
{
187+
$this->expectException(\InvalidArgumentException::class);
188+
$this->expectExceptionMessage('Cannot anonymize less than 0 bytes.');
189+
190+
IpUtils::anonymize('anything', -1, 8);
191+
}
192+
193+
public function testAnonymizeV6WithNegativeBytes()
194+
{
195+
$this->expectException(\InvalidArgumentException::class);
196+
$this->expectExceptionMessage('Cannot anonymize less than 0 bytes.');
197+
198+
IpUtils::anonymize('anything', 1, -1);
199+
}
200+
201+
public function testAnonymizeV4WithTooManyBytes()
202+
{
203+
$this->expectException(\InvalidArgumentException::class);
204+
$this->expectExceptionMessage('Cannot anonymize more than 4 bytes for IPv4 and 16 bytes for IPv6.');
205+
206+
IpUtils::anonymize('anything', 5, 8);
207+
}
208+
209+
public function testAnonymizeV6WithTooManyBytes()
210+
{
211+
$this->expectException(\InvalidArgumentException::class);
212+
$this->expectExceptionMessage('Cannot anonymize more than 4 bytes for IPv4 and 16 bytes for IPv6.');
213+
214+
IpUtils::anonymize('anything', 1, 17);
215+
}
216+
153217
/**
154218
* @dataProvider getIp4SubnetMaskZeroData
155219
*/

0 commit comments

Comments
 (0)