PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.0
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.0
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | includes/class-ip-utils.php +122 -6 2.11.93.0.0 View file →
@@ -39,10 +39,11 @@
39 39 if ( '' === $ip || '' === $pattern ) {
40 40 return false;
41 41 }
42 42
43 - // Exact match (also covers fully-written IPv6).
44 - if ( $ip === $pattern ) {
43 + // Exact match, comparing what the addresses ARE and not how they are
44 + // written (see same_address()).
45 + if ( self::same_address( $ip, $pattern ) ) {
45 46 return true;
46 47 }
47 48
48 49 // CIDR notation.
@@ -113,14 +114,24 @@
113 114 if ( '' === $pattern || false !== strpos( $pattern, '*' ) ) {
114 115 continue;
115 116 }
116 117
117 - if ( $ip === $pattern ) {
118 + if ( self::same_address( $ip, $pattern ) ) {
118 119 return true;
119 120 }
120 121
121 - if ( false !== strpos( $pattern, '/' ) && self::cidr_match( $ip, $pattern ) ) {
122 - return true;
122 + if ( false !== strpos( $pattern, '/' ) ) {
123 + // A range too wide to name anything is ignored here as well as
124 + // rejected on the way in: an entry can arrive by import or from
125 + // an older version, and it must not turn every peer into a
126 + // trusted proxy. See proxy_prefix_is_sane().
127 + if ( ! self::proxy_prefix_is_sane( $pattern ) ) {
128 + continue;
129 + }
130 +
131 + if ( self::cidr_match( $ip, $pattern ) ) {
132 + return true;
133 + }
123 134 }
124 135 }
125 136
126 137 return false;
@@ -238,9 +249,114 @@
238 249 */
239 250 public static function is_valid_proxy( $pattern ) {
240 251 $pattern = trim( (string) $pattern );
241 252
242 - return false === strpos( $pattern, '*' ) && self::is_valid_pattern( $pattern );
253 + if ( false !== strpos( $pattern, '*' ) || ! self::is_valid_pattern( $pattern ) ) {
254 + return false;
255 + }
256 +
257 + return self::proxy_prefix_is_sane( $pattern );
258 + }
259 +
260 + /**
261 + * Whether a CIDR entry is narrow enough to name a proxy
262 + *
263 + * A prefix length of zero matches every address, so 0.0.0.0/0 and ::/0 say
264 + * exactly what the rejected '*' says, written as a CIDR. Rejecting the
265 + * wildcard and accepting those was the same footgun with another spelling:
266 + * with either one in the list every peer counts as a trusted proxy and any
267 + * visitor picks the address the firewall sees, which is the forwarded-header
268 + * spoofing (6.1) this list exists to prevent. Found by the file-by-file
269 + * review of 2.11.10.
270 + *
271 + * Stopping at zero was not enough, and that is the second cross review of
272 + * 2.11.10: 0.0.0.0/1 and 128.0.0.0/1 are two accepted entries that between
273 + * them cover the whole internet, with the same effect and no warning. So the
274 + * question is not "is it zero" but "can this range name a proxy". The floors
275 + * are 8 for IPv4, the widest range that still names something real (the
276 + * classic private network is 10.0.0.0/8), and 7 for IPv6, because fc00::/7 is
277 + * how the whole IPv6 private space is written and this very class treats it
278 + * as the own network in is_own_network(). A first version put the IPv6 floor
279 + * at 16 and refused fc00::/7, fd00::/8 (what Docker hands out) and fe80::/10,
280 + * so a list that already held one of them stopped honouring the header
281 + * altogether and every visitor came out with the proxy's address: the tool
282 + * contradicting itself about what a private network is. Found by the third
283 + * cross review of 2.11.10. With 7, ::/0 and 2000::/3, which is all of the
284 + * routable internet, are still refused.
285 + *
286 + * Measured against what the CDNs publish, and all of them pass: Cloudflare
287 + * (/13, /15, /29), Fastly (/16, /32), Akamai (/10, /11, /13, /24), Sucuri
288 + * (/22, /23), Bunny (/32), CloudFront (/15) and Google (/16, /22).
289 + *
290 + * Kept deliberately as a floor and not as a warning: an entry this wide is
291 + * indistinguishable from the wildcard that is already refused, and the cost
292 + * of being wrong is that any visitor chooses their own address.
293 + *
294 + * @since 2.11.10
295 + *
296 + * @param string $pattern Address or CIDR range.
297 + * @return bool True when it is an exact address or a narrow enough range.
298 + */
299 + public static function proxy_prefix_is_sane( $pattern ) {
300 + $pattern = trim( (string) $pattern );
301 +
302 + if ( false === strpos( $pattern, '/' ) ) {
303 + return true;
304 + }
305 +
306 + $parts = explode( '/', $pattern, 2 );
307 +
308 + if ( 2 !== count( $parts ) ) {
309 + return false;
310 + }
311 +
312 + $subnet = trim( $parts[0] );
313 + $bits = (int) trim( $parts[1] );
314 + $packed = inet_pton( $subnet );
315 +
316 + if ( false === $packed ) {
317 + return false;
318 + }
319 +
320 + $minimo = ( 4 === strlen( $packed ) ) ? 8 : 7;
321 +
322 + return ( $bits >= $minimo );
323 + }
324 +
325 + /**
326 + * Whether two written addresses are the same address
327 + *
328 + * Comparing the strings was enough for IPv4 and wrong for IPv6, where the
329 + * same address has many spellings: 2001:DB8::1, 2001:db8::1 and
330 + * 2001:0db8:0000:0000:0000:0000:0000:0001 are one address written three
331 + * ways, and only the last two compared equal to each other. It mattered
332 + * because the .htaccess side normalises with inet_pton()/inet_ntop() before
333 + * writing its rule, so Apache exempted a peer that PHP did not recognise,
334 + * which is the direction that opens something: measured against a real
335 + * Apache by the second cross review of 2.11.10.
336 + *
337 + * Falls back to the string comparison when either side is not an address, so
338 + * nothing that used to match stops matching.
339 + *
340 + * @since 2.11.10
341 + *
342 + * @param string $a First address.
343 + * @param string $b Second address.
344 + * @return bool
345 + */
346 + public static function same_address( $a, $b ) {
347 + if ( $a === $b ) {
348 + return true;
349 + }
350 +
351 + $pa = inet_pton( $a );
352 + $pb = inet_pton( $b );
353 +
354 + if ( false === $pa || false === $pb ) {
355 + return false;
356 + }
357 +
358 + return ( $pa === $pb );
243 359 }
244 360
245 361 /**
246 362 * Split a list into the proxy addresses that are valid and the ones that are not.