Skip to content

Commit 38b5992

Browse files
feature #49726 [HttpFoundation] Add IpUtils::isPrivateIp (danielburger1337)
This PR was squashed before being merged into the 6.3 branch. Discussion ---------- [HttpFoundation] Add IpUtils::isPrivateIp | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | N/A | License | MIT | Doc PR | symfony/symfony-docs#18102 This is only my second PR for this project, so I hope I followed all the guidelines correctly. Recently I had more and more use cases where I had to make exceptions (mostly rate limiting) for private IP ranges. Symfony currently does not provide an easy way to check if an IP is private or public but implements such logic internally (private constant) for the [NoPrivateNetworkHttpClient](https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php). This PR intents to make the private subnet list reusable by adding it to [IpUtils](https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/HttpFoundation/IpUtils.php). In the original PR of the NoPrivateNetworkHttpClient it was also briefly mentioned that this constant may have value when made public. #35566 (comment) I think symfony should and always should have exposed this constant. Commits ------- 6471582 [HttpFoundation] Add IpUtils::isPrivateIp
2 parents d5a4cb1 + 6471582 commit 38b5992

File tree

5 files changed

+59
-16
lines changed

5 files changed

+59
-16
lines changed

src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php

+1-16
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,6 @@ final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwa
3030
{
3131
use HttpClientTrait;
3232

33-
private const PRIVATE_SUBNETS = [
34-
'127.0.0.0/8',
35-
'10.0.0.0/8',
36-
'192.168.0.0/16',
37-
'172.16.0.0/12',
38-
'169.254.0.0/16',
39-
'0.0.0.0/8',
40-
'240.0.0.0/4',
41-
'::1/128',
42-
'fc00::/7',
43-
'fe80::/10',
44-
'::ffff:0:0/96',
45-
'::/128',
46-
];
47-
4833
private HttpClientInterface $client;
4934
private string|array|null $subnets;
5035

@@ -74,7 +59,7 @@ public function request(string $method, string $url, array $options = []): Respo
7459

7560
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, &$lastPrimaryIp): void {
7661
if ($info['primary_ip'] !== $lastPrimaryIp) {
77-
if ($info['primary_ip'] && IpUtils::checkIp($info['primary_ip'], $subnets ?? self::PRIVATE_SUBNETS)) {
62+
if ($info['primary_ip'] && IpUtils::checkIp($info['primary_ip'], $subnets ?? IpUtils::PRIVATE_SUBNETS)) {
7863
throw new TransportException(sprintf('IP "%s" is blocked for "%s".', $info['primary_ip'], $info['url']));
7964
}
8065

src/Symfony/Component/HttpClient/composer.json

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
"symfony/process": "^5.4|^6.0",
4343
"symfony/stopwatch": "^5.4|^6.0"
4444
},
45+
"conflict": {
46+
"symfony/http-foundation": "<6.3"
47+
},
4548
"autoload": {
4649
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },
4750
"exclude-from-classmap": [

src/Symfony/Component/HttpFoundation/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Create migration for session table when pdo handler is used
1111
* Add support for Relay PHP extension for Redis
1212
* The `Response::sendHeaders()` method now takes an optional HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code)
13+
* Add `IpUtils::isPrivateIp`
1314
* Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`,
1415
* Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set
1516

src/Symfony/Component/HttpFoundation/IpUtils.php

+23
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@
1818
*/
1919
class IpUtils
2020
{
21+
public const PRIVATE_SUBNETS = [
22+
'127.0.0.0/8', // RFC1700 (Loopback)
23+
'10.0.0.0/8', // RFC1918
24+
'192.168.0.0/16', // RFC1918
25+
'172.16.0.0/12', // RFC1918
26+
'169.254.0.0/16', // RFC3927
27+
'0.0.0.0/8', // RFC5735
28+
'240.0.0.0/4', // RFC1112
29+
'::1/128', // Loopback
30+
'fc00::/7', // Unique Local Address
31+
'fe80::/10', // Link Local Address
32+
'::ffff:0:0/96', // IPv4 translations
33+
'::/128', // Unspecified address
34+
];
35+
2136
private static array $checkedIps = [];
2237

2338
/**
@@ -191,4 +206,12 @@ public static function anonymize(string $ip): string
191206

192207
return $ip;
193208
}
209+
210+
/**
211+
* Checks if an IPv4 or IPv6 address is contained in the list of private IP subnets.
212+
*/
213+
public static function isPrivateIp(string $requestIp): bool
214+
{
215+
return self::checkIp($requestIp, self::PRIVATE_SUBNETS);
216+
}
194217
}

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

+31
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,35 @@ public static function getIp4SubnetMaskZeroData()
154154
[false, '1.2.3.4', '256.256.256/0'], // invalid CIDR notation
155155
];
156156
}
157+
158+
/**
159+
* @dataProvider getIsPrivateIpData
160+
*/
161+
public function testIsPrivateIp(string $ip, bool $matches)
162+
{
163+
$this->assertSame($matches, IpUtils::isPrivateIp($ip));
164+
}
165+
166+
public static function getIsPrivateIpData(): array
167+
{
168+
return [
169+
// private
170+
['127.0.0.1', true],
171+
['10.0.0.1', true],
172+
['192.168.0.1', true],
173+
['172.16.0.1', true],
174+
['169.254.0.1', true],
175+
['0.0.0.1', true],
176+
['240.0.0.1', true],
177+
['::1', true],
178+
['fc00::1', true],
179+
['fe80::1', true],
180+
['::ffff:0:1', true],
181+
['fd00::1', true],
182+
183+
// public
184+
['104.26.14.6', false],
185+
['2606:4700:20::681a:e06', false],
186+
];
187+
}
157188
}

0 commit comments

Comments
 (0)