PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / vendor / wpfluent / framework / src / WPFluent / Http / Request / InteractsWithIPTrait.php

InteractsWithIPTrait.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at vendor/wpfluent/framework/src/WPFluent/Http/Request/InteractsWithIPTrait.php

200 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\Framework\Http\Request;
4
5 /**
6 * Trait InteractsWithIPTrait
7 *
8 * Resolves the real client IP address securely.
9 *
10 * SECURITY:
11 * - By default, NO proxies are trusted.
12 * - Forwarded headers are only trusted if REMOTE_ADDR belongs to a trusted proxy.
13 * - Safe for normal hosting, Cloudflare (configured), and explicit proxy setups.
14 *
15 * EXTENSIBILITY:
16 * - Developers can add trusted proxies using the 'trusted_proxies' filter.
17 */
18 trait InteractsWithIPTrait
19 {
20 /**
21 * Cached resolved IP for the current request.
22 *
23 * Instance property (not static) so each Request instance gets its own
24 * cache. This is equivalent in production (one instance per HTTP request)
25 * but avoids cross-request contamination in tests where multiple dispatches
26 * share the same PHP process.
27 *
28 * @var string|null
29 */
30 protected $resolvedIp = null;
31
32 /**
33 * Get client IP.
34 *
35 * @param bool $anonymize Return anonymized IP if true.
36 * @return string
37 */
38 public function getIp($anonymize = false)
39 {
40 if ($this->resolvedIp === null) {
41 $this->resolvedIp = $this->resolveIp();
42 }
43
44 $ip = $anonymize
45 ? wp_privacy_anonymize_ip($this->resolvedIp)
46 : $this->resolvedIp;
47
48 return $this->app->applyCustomFilters('user_ip', $ip, $anonymize);
49 }
50
51 /**
52 * Resolve the real client IP.
53 *
54 * @return string
55 */
56 protected function resolveIp()
57 {
58 if (empty($_SERVER['REMOTE_ADDR'])) {
59 return '127.0.0.1'; // CLI or unusual environment
60 }
61
62 $remoteAddr = $this->sanitize($_SERVER['REMOTE_ADDR']);
63
64 // Only trust forwarded headers if REMOTE_ADDR is a trusted proxy
65 if (!$this->isTrustedProxy($remoteAddr)) {
66 return $this->isValidIp($remoteAddr) ? $remoteAddr : '127.0.0.1';
67 }
68
69 // Trusted proxy detected → check forwarded headers
70
71 // Cloudflare header
72 if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
73 $cfIp = $this->sanitize($_SERVER['HTTP_CF_CONNECTING_IP']);
74 if ($this->isValidIp($cfIp)) {
75 return $cfIp;
76 }
77 }
78
79 // X-Forwarded-For header (may contain multiple IPs)
80 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
81 $forwarded = $this->sanitize($_SERVER['HTTP_X_FORWARDED_FOR']);
82 $ips = explode(',', $forwarded);
83 foreach ($ips as $ip) {
84 $ip = trim($ip);
85 if ($this->isValidIp($ip)) {
86 return $ip;
87 }
88 }
89 }
90
91 // X-Real-IP fallback
92 if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
93 $realIp = $this->sanitize($_SERVER['HTTP_X_REAL_IP']);
94 if ($this->isValidIp($realIp)) {
95 return $realIp;
96 }
97 }
98
99 // Fallback to REMOTE_ADDR
100 return $this->isValidIp($remoteAddr) ? $remoteAddr : '127.0.0.1';
101 }
102
103 /**
104 * Determine if REMOTE_ADDR belongs to a trusted proxy.
105 *
106 * @param string $ip
107 * @return bool
108 */
109 protected function isTrustedProxy($ip)
110 {
111 if (!$this->isValidIp($ip)) {
112 return false;
113 }
114
115 foreach ($this->getTrustedProxies() as $range) {
116 if ($this->ipInRange($ip, $range)) {
117 return true;
118 }
119 }
120
121 return false;
122 }
123
124 /**
125 * Get an array of trusted proxy CIDR ranges.
126 *
127 * Reads from config/trustedproxy.php (proxies key) by default.
128 * Can be overridden or extended using the filter:
129 * add_filter('trusted_proxies', fn($proxies) => [...$proxies, '10.0.0.1']);
130 *
131 * @return array
132 */
133 protected function getTrustedProxies()
134 {
135 $configured = $this->app->config->get('trustedproxy.proxies', []);
136 return $this->app->applyCustomFilters('trusted_proxies', $configured);
137 }
138
139 /**
140 * Validate IPv4 or IPv6 address.
141 *
142 * @param string $ip
143 * @return bool
144 */
145 protected function isValidIp($ip)
146 {
147 return (bool) filter_var($ip, FILTER_VALIDATE_IP);
148 }
149
150 /**
151 * Sanitize server values (WordPress compatible).
152 *
153 * @param string $value
154 * @return string
155 */
156 protected function sanitize($value)
157 {
158 return sanitize_text_field(wp_unslash($value));
159 }
160
161 /**
162 * Check if an IPv4 address is within a CIDR range.
163 *
164 * @param string $ip IP address to check (e.g., "173.245.50.10")
165 * @param string $cidr CIDR range (e.g., "173.245.48.0/20")
166 * @return bool True if IP is inside the range, false otherwise
167 */
168 protected function ipInRange($ip, $cidr)
169 {
170 // If there is no slash, treat CIDR as a single IP
171 if (strpos($cidr, '/') === false) {
172 return $ip === $cidr;
173 }
174
175 // Split CIDR into subnet and mask length
176 list($subnet, $maskLength) = explode('/', $cidr);
177
178 // Convert IP and subnet to long integers (32-bit numbers)
179 $ipLong = ip2long($ip);
180 $subnetLong = ip2long($subnet);
181
182 // If conversion fails (invalid IP), return false
183 if ($ipLong === false || $subnetLong === false) {
184 return false;
185 }
186
187 // Create a netmask with $maskLength ones on the left
188 // Example: /20 → 11111111.11111111.11110000.00000000
189 $mask = -1 << (32 - (int) $maskLength);
190
191 // Apply netmask to subnet to get canonical network address
192 // Clears host bits to ensure proper comparison
193 $network = $subnetLong & $mask;
194
195 // Apply same mask to target IP and compare with network
196 // If equal → IP is inside the CIDR range
197 return ($ipLong & $mask) === $network;
198 }
199 }
200