PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.4
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.4
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
vigilante / includes / class-ip-utils.php

class-ip-utils.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.9.4, at includes/class-ip-utils.php

266 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * IP matching utilities
4 *
5 * Shared IP/pattern matching for the firewall and login modules. Supports
6 * exact addresses, CIDR ranges and wildcards, for both IPv4 and IPv6.
7 *
8 * @package Vigilante
9 */
10
11 // Prevent direct access
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * Class Vigilante_IP_Utils
18 *
19 * Stateless helpers. All methods are static.
20 */
21 class Vigilante_IP_Utils {
22
23 /**
24 * Check whether an IP matches a single pattern.
25 *
26 * Supported pattern forms (IPv4 and IPv6 alike):
27 * - Exact address: 203.0.113.5 / 2a02:c207::1
28 * - CIDR range: 203.0.113.0/24 / 2a02:c207::/32
29 * - Wildcard: 203.0.113.* / 2a02:c207:*
30 *
31 * @param string $ip IP address to test.
32 * @param string $pattern Pattern to match against.
33 * @return bool True on match.
34 */
35 public static function matches( $ip, $pattern ) {
36 $ip = trim( (string) $ip );
37 $pattern = trim( (string) $pattern );
38
39 if ( '' === $ip || '' === $pattern ) {
40 return false;
41 }
42
43 // Exact match (also covers fully-written IPv6).
44 if ( $ip === $pattern ) {
45 return true;
46 }
47
48 // CIDR notation.
49 if ( false !== strpos( $pattern, '/' ) ) {
50 return self::cidr_match( $ip, $pattern );
51 }
52
53 // Wildcard notation.
54 if ( false !== strpos( $pattern, '*' ) ) {
55 return self::wildcard_match( $ip, $pattern );
56 }
57
58 return false;
59 }
60
61 /**
62 * Check whether an IP matches any pattern in a list.
63 *
64 * @param string $ip IP address to test.
65 * @param array $list List of patterns.
66 * @return bool True if any pattern matches.
67 */
68 public static function in_list( $ip, $list ) {
69 if ( empty( $list ) || ! is_array( $list ) ) {
70 return false;
71 }
72
73 foreach ( $list as $pattern ) {
74 if ( self::matches( $ip, (string) $pattern ) ) {
75 return true;
76 }
77 }
78
79 return false;
80 }
81
82 /**
83 * Match an IP against a CIDR range. Works for IPv4 and IPv6.
84 *
85 * The comparison is done on the packed binary form, so the textual
86 * representation of an IPv6 address (compressed or not) does not matter.
87 *
88 * @param string $ip IP address to test.
89 * @param string $cidr CIDR range (e.g. 203.0.113.0/24 or 2a02::/32).
90 * @return bool True on match.
91 */
92 private static function cidr_match( $ip, $cidr ) {
93 $parts = explode( '/', $cidr, 2 );
94 if ( 2 !== count( $parts ) ) {
95 return false;
96 }
97
98 $subnet = trim( $parts[0] );
99 $bits = trim( $parts[1] );
100
101 // Prefix length must be a plain integer.
102 if ( '' === $bits || ! ctype_digit( $bits ) ) {
103 return false;
104 }
105 $bits = (int) $bits;
106
107 // Validate both addresses before packing so inet_pton never warns.
108 if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) || ! filter_var( $subnet, FILTER_VALIDATE_IP ) ) {
109 return false;
110 }
111
112 $ip_packed = inet_pton( $ip );
113 $subnet_packed = inet_pton( $subnet );
114 if ( false === $ip_packed || false === $subnet_packed ) {
115 return false;
116 }
117
118 // Different address family (4 bytes for IPv4, 16 for IPv6).
119 if ( strlen( $ip_packed ) !== strlen( $subnet_packed ) ) {
120 return false;
121 }
122
123 $max_bits = strlen( $ip_packed ) * 8;
124 if ( $bits < 0 || $bits > $max_bits ) {
125 return false;
126 }
127
128 // Compare whole bytes first.
129 $whole_bytes = intdiv( $bits, 8 );
130 if ( $whole_bytes > 0 && substr( $ip_packed, 0, $whole_bytes ) !== substr( $subnet_packed, 0, $whole_bytes ) ) {
131 return false;
132 }
133
134 // Then the remaining bits of the partial byte, if any.
135 $remaining = $bits % 8;
136 if ( $remaining > 0 ) {
137 $mask = 0xFF << ( 8 - $remaining ) & 0xFF;
138 $ip_byte = ord( $ip_packed[ $whole_bytes ] );
139 $sub_byte = ord( $subnet_packed[ $whole_bytes ] );
140 if ( ( $ip_byte & $mask ) !== ( $sub_byte & $mask ) ) {
141 return false;
142 }
143 }
144
145 return true;
146 }
147
148 /**
149 * Match an IP against a wildcard pattern (e.g. 203.0.113.* or 2a02:c207:*).
150 *
151 * Operates on the textual form. The '*' stands for any run of characters;
152 * every other character is matched literally, so it works for the dots of
153 * IPv4 and the colons of IPv6.
154 *
155 * @param string $ip IP address to test.
156 * @param string $pattern Wildcard pattern.
157 * @return bool True on match.
158 */
159 private static function wildcard_match( $ip, $pattern ) {
160 $quoted = preg_quote( $pattern, '/' );
161 $regex = '/^' . str_replace( '\*', '.*', $quoted ) . '$/';
162
163 return (bool) preg_match( $regex, $ip );
164 }
165
166 /**
167 * Proxy headers an admin may declare as trusted, mapped to their $_SERVER key.
168 *
169 * @return array<string,string>
170 */
171 public static function trusted_header_map() {
172 return array(
173 'cf-connecting-ip' => 'HTTP_CF_CONNECTING_IP',
174 'x-forwarded-for' => 'HTTP_X_FORWARDED_FOR',
175 'x-real-ip' => 'HTTP_X_REAL_IP',
176 );
177 }
178
179 /**
180 * The proxy header the admin has declared as trusted, or '' for none.
181 *
182 * @return string
183 */
184 public static function trusted_proxy_header() {
185 $options = get_option( 'vigilante_options' );
186 if ( is_array( $options ) && ! empty( $options['firewall']['trusted_proxy_header'] ) ) {
187 $header = (string) $options['firewall']['trusted_proxy_header'];
188 if ( isset( self::trusted_header_map()[ $header ] ) ) {
189 return $header;
190 }
191 }
192 return '';
193 }
194
195 /**
196 * Resolve the client IP from a $_SERVER-like array.
197 *
198 * Only the real TCP peer (REMOTE_ADDR) is trusted by default, because it
199 * cannot be spoofed. A forwarded-for / connecting-ip header is honoured
200 * ONLY when the admin has explicitly declared their site sits behind that
201 * proxy; otherwise any visitor could forge the header and impersonate any
202 * IP (bypassing the whitelist, evading the blacklist, poisoning the rate
203 * limiter, etc.).
204 *
205 * @param array $server A $_SERVER-like array.
206 * @param string $trusted_header One of the keys in trusted_header_map(), or '' for none.
207 * @return string Validated IP, or '0.0.0.0' when none could be determined.
208 */
209 public static function resolve_client_ip( $server, $trusted_header = '' ) {
210 $map = self::trusted_header_map();
211
212 if ( '' !== $trusted_header && isset( $map[ $trusted_header ] ) ) {
213 $key = $map[ $trusted_header ];
214 if ( ! empty( $server[ $key ] ) ) {
215 $value = (string) $server[ $key ];
216 // X-Forwarded-For may be a "client, proxy1, proxy2" chain; the
217 // original client is the first entry.
218 if ( false !== strpos( $value, ',' ) ) {
219 $parts = explode( ',', $value );
220 $value = $parts[0];
221 }
222 $value = trim( $value );
223 if ( filter_var( $value, FILTER_VALIDATE_IP ) ) {
224 return $value;
225 }
226 }
227 }
228
229 if ( ! empty( $server['REMOTE_ADDR'] ) ) {
230 $remote = trim( (string) $server['REMOTE_ADDR'] );
231 if ( filter_var( $remote, FILTER_VALIDATE_IP ) ) {
232 return $remote;
233 }
234 }
235
236 return '0.0.0.0';
237 }
238
239 /**
240 * Current request client IP, honouring the configured trusted proxy header.
241 *
242 * Reads only the needed headers, each sanitized at the point of access, so
243 * the input-sanitization sniff is satisfied without any suppression.
244 *
245 * @return string
246 */
247 public static function get_client_ip() {
248 $trusted = self::trusted_proxy_header();
249 $server = array();
250
251 if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
252 $server['REMOTE_ADDR'] = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
253 }
254
255 if ( '' !== $trusted ) {
256 $map = self::trusted_header_map();
257 $key = $map[ $trusted ];
258 if ( isset( $_SERVER[ $key ] ) ) {
259 $server[ $key ] = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
260 }
261 }
262
263 return self::resolve_client_ip( $server, $trusted );
264 }
265 }
266