| @@ -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. |