Skip to content

Commit 2b6fdfd

Browse files
danielburger1337nicolas-grekas
authored andcommitted
[HttpFoundation] Clear IpUtils cache to prevent memory leaks
1 parent a384f87 commit 2b6fdfd

File tree

2 files changed

+59
-16
lines changed

2 files changed

+59
-16
lines changed

IpUtils.php

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,34 +75,34 @@ public static function checkIp(string $requestIp, string|array $ips): bool
7575
public static function checkIp4(string $requestIp, string $ip): bool
7676
{
7777
$cacheKey = $requestIp.'-'.$ip.'-v4';
78-
if (isset(self::$checkedIps[$cacheKey])) {
79-
return self::$checkedIps[$cacheKey];
78+
if (null !== $cacheValue = self::getCacheResult($cacheKey)) {
79+
return $cacheValue;
8080
}
8181

8282
if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
83-
return self::$checkedIps[$cacheKey] = false;
83+
return self::setCacheResult($cacheKey, false);
8484
}
8585

8686
if (str_contains($ip, '/')) {
8787
[$address, $netmask] = explode('/', $ip, 2);
8888

8989
if ('0' === $netmask) {
90-
return self::$checkedIps[$cacheKey] = false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4);
90+
return self::setCacheResult($cacheKey, false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4));
9191
}
9292

9393
if ($netmask < 0 || $netmask > 32) {
94-
return self::$checkedIps[$cacheKey] = false;
94+
return self::setCacheResult($cacheKey, false);
9595
}
9696
} else {
9797
$address = $ip;
9898
$netmask = 32;
9999
}
100100

101101
if (false === ip2long($address)) {
102-
return self::$checkedIps[$cacheKey] = false;
102+
return self::setCacheResult($cacheKey, false);
103103
}
104104

105-
return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
105+
return self::setCacheResult($cacheKey, 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask));
106106
}
107107

108108
/**
@@ -120,8 +120,8 @@ public static function checkIp4(string $requestIp, string $ip): bool
120120
public static function checkIp6(string $requestIp, string $ip): bool
121121
{
122122
$cacheKey = $requestIp.'-'.$ip.'-v6';
123-
if (isset(self::$checkedIps[$cacheKey])) {
124-
return self::$checkedIps[$cacheKey];
123+
if (null !== $cacheValue = self::getCacheResult($cacheKey)) {
124+
return $cacheValue;
125125
}
126126

127127
if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) {
@@ -130,26 +130,26 @@ public static function checkIp6(string $requestIp, string $ip): bool
130130

131131
// Check to see if we were given a IP4 $requestIp or $ip by mistake
132132
if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
133-
return self::$checkedIps[$cacheKey] = false;
133+
return self::setCacheResult($cacheKey, false);
134134
}
135135

136136
if (str_contains($ip, '/')) {
137137
[$address, $netmask] = explode('/', $ip, 2);
138138

139139
if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
140-
return self::$checkedIps[$cacheKey] = false;
140+
return self::setCacheResult($cacheKey, false);
141141
}
142142

143143
if ('0' === $netmask) {
144144
return (bool) unpack('n*', @inet_pton($address));
145145
}
146146

147147
if ($netmask < 1 || $netmask > 128) {
148-
return self::$checkedIps[$cacheKey] = false;
148+
return self::setCacheResult($cacheKey, false);
149149
}
150150
} else {
151151
if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
152-
return self::$checkedIps[$cacheKey] = false;
152+
return self::setCacheResult($cacheKey, false);
153153
}
154154

155155
$address = $ip;
@@ -160,19 +160,19 @@ public static function checkIp6(string $requestIp, string $ip): bool
160160
$bytesTest = unpack('n*', @inet_pton($requestIp));
161161

162162
if (!$bytesAddr || !$bytesTest) {
163-
return self::$checkedIps[$cacheKey] = false;
163+
return self::setCacheResult($cacheKey, false);
164164
}
165165

166166
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
167167
$left = $netmask - 16 * ($i - 1);
168168
$left = ($left <= 16) ? $left : 16;
169169
$mask = ~(0xFFFF >> $left) & 0xFFFF;
170170
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
171-
return self::$checkedIps[$cacheKey] = false;
171+
return self::setCacheResult($cacheKey, false);
172172
}
173173
}
174174

175-
return self::$checkedIps[$cacheKey] = true;
175+
return self::setCacheResult($cacheKey, true);
176176
}
177177

178178
/**
@@ -214,4 +214,28 @@ public static function isPrivateIp(string $requestIp): bool
214214
{
215215
return self::checkIp($requestIp, self::PRIVATE_SUBNETS);
216216
}
217+
218+
private static function getCacheResult(string $cacheKey): ?bool
219+
{
220+
if (isset(self::$checkedIps[$cacheKey])) {
221+
// Move the item last in cache (LRU)
222+
$value = self::$checkedIps[$cacheKey];
223+
unset(self::$checkedIps[$cacheKey]);
224+
self::$checkedIps[$cacheKey] = $value;
225+
226+
return self::$checkedIps[$cacheKey];
227+
}
228+
229+
return null;
230+
}
231+
232+
private static function setCacheResult(string $cacheKey, bool $result): bool
233+
{
234+
if (1000 < \count(self::$checkedIps)) {
235+
// stop memory leak if there are many keys
236+
self::$checkedIps = \array_slice(self::$checkedIps, 500, null, true);
237+
}
238+
239+
return self::$checkedIps[$cacheKey] = $result;
240+
}
217241
}

Tests/IpUtilsTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,23 @@ public static function getIsPrivateIpData(): array
200200
['2606:4700:20::681a:e06', false],
201201
];
202202
}
203+
204+
public function testCacheSizeLimit()
205+
{
206+
$ref = new \ReflectionClass(IpUtils::class);
207+
208+
/** @var array */
209+
$checkedIps = $ref->getStaticPropertyValue('checkedIps');
210+
$this->assertIsArray($checkedIps);
211+
212+
$maxCheckedIps = 1000;
213+
214+
for ($i = 1; $i < $maxCheckedIps * 1.5; ++$i) {
215+
$ip = '192.168.1.'.str_pad((string) $i, 3, '0');
216+
217+
IpUtils::checkIp4($ip, '127.0.0.1');
218+
}
219+
220+
$this->assertLessThan($maxCheckedIps, \count($checkedIps));
221+
}
203222
}

0 commit comments

Comments
 (0)