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.6.1 5.6.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 All 36 releases
← All changes | core/IPHelper.class.php +103 -31 3.0.715.5.0 View file →
@@ -1,31 +1,103 @@
1 -<?php
2 -
3 -namespace forge12\contactform7\CF7DoubleOptIn;
4 -
5 -if ( ! defined( 'ABSPATH' ) ) {
6 - exit;
7 -}
8 -
9 -class IPHelper {
10 - /**
11 - * Retrieves the IP address of the current client.
12 - *
13 - * The method checks if the IP address is obtained from a shared internet, proxy, or remote address.
14 - *
15 - * @return string The IP address of the current client.
16 - */
17 - public static function getIPAdress(): string {
18 - //whether ip is from share internet
19 - if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
20 - $ip_address = sanitize_text_field( $_SERVER['HTTP_CLIENT_IP'] );
21 - } //whether ip is from proxy
22 - elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
23 - $ip_address = sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_FOR'] );
24 - } //whether ip is from remote address
25 - else {
26 - $ip_address = sanitize_text_field( $_SERVER['REMOTE_ADDR'] );
27 - }
28 -
29 - return $ip_address;
30 - }
31 -}
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 +}