PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
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 +24 -330 3.0.02.11.8 View file →
@@ -39,11 +39,10 @@
39 39 if ( '' === $ip || '' === $pattern ) {
40 40 return false;
41 41 }
42 42
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 ) ) {
43 + // Exact match (also covers fully-written IPv6).
44 + if ( $ip === $pattern ) {
46 45 return true;
47 46 }
48 47
49 48 // CIDR notation.
@@ -80,65 +79,8 @@
80 79 return false;
81 80 }
82 81
83 82 /**
84 - * Whether an address is in a list, matching exact addresses and CIDR only.
85 - *
86 - * The strict cousin of in_list(), for deciding identity rather than
87 - * filtering traffic. A wildcard entry (1.2.* or a bare *) is never honoured
88 - * here: a proxy the site delegates its client IP to is a specific machine
89 - * or a specific range, and a wildcard in that role is the trust-everyone
90 - * footgun that would reopen the forwarded-header spoofing (6.1) this release
91 - * closes, since matches() turns a bare * into /^.*$/ and trusts every peer.
92 - * Kept apart from in_list() on purpose, so the firewall whitelist keeps its
93 - * wildcards while the proxy-trust decision cannot grow one.
94 - *
95 - * @since 2.11.9
96 - *
97 - * @param string $ip Address to test.
98 - * @param array $list List of exact addresses or CIDR ranges.
99 - * @return bool
100 - */
101 - public static function in_list_ip_or_cidr( $ip, $list ) {
102 - if ( empty( $list ) || ! is_array( $list ) ) {
103 - return false;
104 - }
105 -
106 - $ip = trim( (string) $ip );
107 - if ( '' === $ip ) {
108 - return false;
109 - }
110 -
111 - foreach ( $list as $pattern ) {
112 - $pattern = trim( (string) $pattern );
113 -
114 - if ( '' === $pattern || false !== strpos( $pattern, '*' ) ) {
115 - continue;
116 - }
117 -
118 - if ( self::same_address( $ip, $pattern ) ) {
119 - return true;
120 - }
121 -
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 - }
134 - }
135 - }
136 -
137 - return false;
138 - }
139 -
140 - /**
141 83 * Whether a string is a pattern this class can actually match.
142 84 *
143 85 * The counterpart of matches(): everything this returns true for is
144 86 * something the matcher understands, and everything else is noise that
@@ -234,171 +176,8 @@
234 176 );
235 177 }
236 178
237 179 /**
238 - * Whether a string is a proxy address this class trusts to set a header.
239 - *
240 - * A valid pattern that is not a wildcard: an exact address or a CIDR range.
241 - * The counterpart of in_list_ip_or_cidr() for the save path, so a wildcard
242 - * typed into the trusted proxies field is rejected with feedback instead of
243 - * sitting there matching nothing (or, before the strict matcher, everything).
244 - *
245 - * @since 2.11.9
246 - *
247 - * @param string $pattern Candidate pattern.
248 - * @return bool
249 - */
250 - public static function is_valid_proxy( $pattern ) {
251 - $pattern = trim( (string) $pattern );
252 -
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 );
359 - }
360 -
361 - /**
362 - * Split a list into the proxy addresses that are valid and the ones that are not.
363 - *
364 - * Like split_list(), but rejecting wildcards: the trusted proxies list feeds
365 - * an identity decision, and only exact addresses and CIDR ranges belong there.
366 - *
367 - * @since 2.11.9
368 - *
369 - * @param array|string $list List of patterns, or a newline separated string.
370 - * @return array{valid: string[], rejected: string[]}
371 - */
372 - public static function split_list_ip_or_cidr( $list ) {
373 - if ( is_string( $list ) ) {
374 - $list = preg_split( '/[\r\n]+/', $list );
375 - }
376 -
377 - $valid = array();
378 - $rejected = array();
379 -
380 - foreach ( (array) $list as $entry ) {
381 - $entry = trim( (string) $entry );
382 -
383 - if ( '' === $entry ) {
384 - continue;
385 - }
386 -
387 - if ( self::is_valid_proxy( $entry ) ) {
388 - $valid[] = $entry;
389 - } else {
390 - $rejected[] = $entry;
391 - }
392 - }
393 -
394 - return array(
395 - 'valid' => array_values( array_unique( $valid ) ),
396 - 'rejected' => array_values( array_unique( $rejected ) ),
397 - );
398 - }
399 -
400 - /**
401 180 * Whether a wildcard pattern is plausible for one address family.
402 181 *
403 182 * A bare '*' is rejected on purpose: as a whitelist it would let everyone
404 183 * in and as a blacklist it would lock everyone out, and nobody types that
@@ -577,127 +356,42 @@
577 356 return '';
578 357 }
579 358
580 359 /**
581 - * The proxy IPs/CIDRs the admin declared their forwarded header comes from.
582 - *
583 - * @since 2.11.9
584 - *
585 - * @return string[]
586 - */
587 - public static function trusted_proxies() {
588 - $options = get_option( 'vigilante_options' );
589 - $list = ( is_array( $options ) && isset( $options['firewall']['trusted_proxies'] ) ) ? $options['firewall']['trusted_proxies'] : array();
590 - return is_array( $list ) ? $list : array();
591 - }
592 -
593 - /**
594 - * Cloudflare's published edge ranges, so CF-Connecting-IP verifies itself.
595 - *
596 - * From https://www.cloudflare.com/ips/ (stable, changes rarely). Bundled so
597 - * a site behind Cloudflare does not have to list them by hand; if they ever
598 - * change, the admin can add the new ones to the trusted proxies list.
599 - *
600 - * @since 2.11.9
601 - *
602 - * @return string[]
603 - */
604 - public static function cloudflare_ranges() {
605 - return array(
606 - '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22',
607 - '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20',
608 - '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13',
609 - '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
610 - '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32',
611 - '2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32',
612 - );
613 - }
614 -
615 - /**
616 - * Whether the TCP peer may be trusted to have set the forwarded header
617 - *
618 - * The reviewer of 2.11.8 was right: honouring CF-Connecting-IP,
619 - * X-Forwarded-For or X-Real-IP without checking who sent them lets any
620 - * visitor whose request reaches PHP directly forge the address the firewall,
621 - * the whitelist and the rate limiter act on. So the header is honoured only
622 - * when the real connection, REMOTE_ADDR, is a proxy we have reason to trust:
623 - *
624 - * - an exact address or CIDR range in the admin's trusted proxies list
625 - * (wins for any header); a wildcard there is ignored, see
626 - * in_list_ip_or_cidr();
627 - * - for CF-Connecting-IP, one of Cloudflare's published ranges, since only
628 - * Cloudflare sends that header;
629 - * - with no list configured, an address of your own network (a reverse proxy
630 - * in front of PHP, a load balancer in a private subnet), which a visitor
631 - * hitting a public origin directly is not.
632 - *
633 - * A public load balancer that connects from a public address needs its IPs
634 - * in the trusted proxies list; until then its header is not honoured and the
635 - * connection address is used, which is safe.
636 - *
637 - * @since 2.11.9
638 - *
639 - * @param string $remote Validated REMOTE_ADDR.
640 - * @param string $header Trusted header key.
641 - * @param string[] $trusted_proxies Configured proxy IPs/CIDRs.
642 - * @return bool
643 - */
644 - private static function peer_is_trusted_proxy( $remote, $header, $trusted_proxies ) {
645 - // A dual-stack proxy connects as ::ffff:10.0.0.5; read it as the IPv4 it
646 - // is, so a private reverse proxy is recognised as own network and a peer
647 - // listed by its IPv4 matches. client_from_chain() already unmaps, this
648 - // keeps the two sides symmetric (found by the cross review of 2.11.9).
649 - $remote = self::unmap_ipv4( $remote );
650 -
651 - if ( ! empty( $trusted_proxies ) && self::in_list_ip_or_cidr( $remote, $trusted_proxies ) ) {
652 - return true;
653 - }
654 -
655 - if ( 'cf-connecting-ip' === $header && self::in_list_ip_or_cidr( $remote, self::cloudflare_ranges() ) ) {
656 - return true;
657 - }
658 -
659 - return empty( $trusted_proxies ) && self::is_own_network( $remote );
660 - }
661 -
662 - /**
663 360 * Resolve the client IP from a $_SERVER-like array.
664 361 *
665 362 * Only the real TCP peer (REMOTE_ADDR) is trusted by default, because it
666 - * cannot be spoofed. A forwarded-for / connecting-ip header is honoured only
667 - * when the admin has declared their site sits behind that proxy AND the
668 - * connection actually comes from a proxy we trust (see
669 - * peer_is_trusted_proxy()); otherwise any visitor could forge the header and
670 - * impersonate any IP, bypassing the whitelist, evading the blacklist and
671 - * poisoning the rate limiter. Reported by the wp.org review of 2.11.8.
363 + * cannot be spoofed. A forwarded-for / connecting-ip header is honoured
364 + * ONLY when the admin has explicitly declared their site sits behind that
365 + * proxy; otherwise any visitor could forge the header and impersonate any
366 + * IP (bypassing the whitelist, evading the blacklist, poisoning the rate
367 + * limiter, etc.).
672 368 *
673 - * @param array $server A $_SERVER-like array.
674 - * @param string $trusted_header One of the keys in trusted_header_map(), or '' for none.
675 - * @param string[] $trusted_proxies Configured proxy IPs/CIDRs.
369 + * @param array $server A $_SERVER-like array.
370 + * @param string $trusted_header One of the keys in trusted_header_map(), or '' for none.
676 371 * @return string Validated IP, or '0.0.0.0' when none could be determined.
677 372 */
678 - public static function resolve_client_ip( $server, $trusted_header = '', $trusted_proxies = array() ) {
679 - $map = self::trusted_header_map();
680 - $remote = '';
373 + public static function resolve_client_ip( $server, $trusted_header = '' ) {
374 + $map = self::trusted_header_map();
681 375
682 - if ( ! empty( $server['REMOTE_ADDR'] ) ) {
683 - $candidate = trim( (string) $server['REMOTE_ADDR'] );
684 - if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
685 - $remote = $candidate;
376 + if ( '' !== $trusted_header && isset( $map[ $trusted_header ] ) ) {
377 + $key = $map[ $trusted_header ];
378 + if ( ! empty( $server[ $key ] ) ) {
379 + $value = self::client_from_chain( (string) $server[ $key ] );
380 + if ( '' !== $value ) {
381 + return $value;
382 + }
686 383 }
687 384 }
688 385
689 - if ( '' !== $trusted_header && isset( $map[ $trusted_header ] ) && '' !== $remote
690 - && ! empty( $server[ $map[ $trusted_header ] ] )
691 - && self::peer_is_trusted_proxy( $remote, $trusted_header, $trusted_proxies )
692 - ) {
693 - $value = self::client_from_chain( (string) $server[ $map[ $trusted_header ] ] );
694 - if ( '' !== $value ) {
695 - return $value;
386 + if ( ! empty( $server['REMOTE_ADDR'] ) ) {
387 + $remote = trim( (string) $server['REMOTE_ADDR'] );
388 + if ( filter_var( $remote, FILTER_VALIDATE_IP ) ) {
389 + return $remote;
696 390 }
697 391 }
698 392
699 - return '' !== $remote ? $remote : '0.0.0.0';
393 + return '0.0.0.0';
700 394 }
701 395
702 396 /**
703 397 * The visitor address in a forwarded header, read from the proxy's end
@@ -865,7 +559,7 @@
865 559 $server[ $key ] = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
866 560 }
867 561 }
868 562
869 - return self::resolve_client_ip( $server, $trusted, self::trusted_proxies() );
563 + return self::resolve_client_ip( $server, $trusted );
870 564 }
871 565 }