PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.5.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.5.0
5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 3.1.0 All 34 releases
double-opt-in / core / IPHelper.class.php

IPHelper.class.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.5.0, at core/IPHelper.class.php

104 lines 3.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace forge12\contactform7\CF7DoubleOptIn;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 class IPHelper {
10 /**
11 * Resolve the current client IP.
12 *
13 * Security: `REMOTE_ADDR` is the only value a remote client cannot spoof.
14 * `X-Forwarded-For` (and friends) are attacker-controllable, so we only
15 * consult XFF when the direct peer (`REMOTE_ADDR`) is itself a trusted
16 * proxy — configured via the `f12_doi_trusted_proxies` filter (list of
17 * IPs / CIDR ranges). Default is an EMPTY list, i.e. REMOTE_ADDR only.
18 * Sites behind a CDN/reverse proxy must register their proxy ranges:
19 *
20 * add_filter( 'f12_doi_trusted_proxies', fn() => array( '173.245.48.0/20' ) );
21 *
22 * Rationale: many WordPress plugins were IP-spoofable by trusting XFF
23 * blindly — this value feeds the opt-in rate limiter and is stored as
24 * GDPR consent evidence, so it must not be forgeable.
25 *
26 * @return string
27 */
28 public static function getIPAdress(): string {
29 $trusted = function_exists( 'apply_filters' )
30 ? (array) apply_filters( 'f12_doi_trusted_proxies', array() )
31 : array();
32
33 return self::resolveClientIp( isset( $_SERVER ) ? (array) $_SERVER : array(), $trusted );
34 }
35
36 /**
37 * Pure IP resolution — no WordPress dependency, so it is unit-testable.
38 *
39 * @param array $server A `$_SERVER`-shaped array.
40 * @param array $trustedProxies IPs / IPv4-CIDR ranges considered trusted.
41 * @return string A validated IP, or '' when nothing valid is present.
42 */
43 public static function resolveClientIp( array $server, array $trustedProxies = array() ): string {
44 $remote = isset( $server['REMOTE_ADDR'] ) ? trim( (string) $server['REMOTE_ADDR'] ) : '';
45 $remote = filter_var( $remote, FILTER_VALIDATE_IP ) ? $remote : '';
46
47 // Only walk the forwarded chain when the direct peer is a known proxy.
48 if ( $remote !== ''
49 && ! empty( $trustedProxies )
50 && self::ipInRanges( $remote, $trustedProxies )
51 && ! empty( $server['HTTP_X_FORWARDED_FOR'] ) ) {
52
53 $hops = array_reverse(
54 array_map( 'trim', explode( ',', (string) $server['HTTP_X_FORWARDED_FOR'] ) )
55 );
56 foreach ( $hops as $hop ) {
57 // First hop that is a valid IP and NOT itself a trusted proxy
58 // is the real client.
59 if ( filter_var( $hop, FILTER_VALIDATE_IP ) && ! self::ipInRanges( $hop, $trustedProxies ) ) {
60 return $hop;
61 }
62 }
63 }
64
65 return $remote;
66 }
67
68 /**
69 * Match an IP against a list of exact IPs and IPv4 CIDR ranges.
70 * (IPv6 is matched exactly; extend here if IPv6 CIDR is needed.)
71 *
72 * @param string $ip
73 * @param array $ranges
74 * @return bool
75 */
76 private static function ipInRanges( string $ip, array $ranges ): bool {
77 foreach ( $ranges as $range ) {
78 $range = trim( (string) $range );
79 if ( $range === '' ) {
80 continue;
81 }
82 if ( strpos( $range, '/' ) === false ) {
83 if ( $ip === $range ) {
84 return true;
85 }
86 continue;
87 }
88
89 list( $subnet, $bits ) = array_pad( explode( '/', $range, 2 ), 2, '' );
90 $bits = (int) $bits;
91 $ipLong = ip2long( $ip );
92 $subnetLong = ip2long( $subnet );
93 if ( $ipLong === false || $subnetLong === false || $bits < 0 || $bits > 32 ) {
94 continue; // Not IPv4 or malformed CIDR — skip.
95 }
96 $mask = $bits === 0 ? 0 : ( -1 << ( 32 - $bits ) );
97 if ( ( $ipLong & $mask ) === ( $subnetLong & $mask ) ) {
98 return true;
99 }
100 }
101 return false;
102 }
103 }
104