| @@ -55,9 +55,9 @@ | ||
| 55 | 55 | * @param string $endpoint_name Endpoint identifier |
| 56 | 56 | * @param int $user_id User ID (0 for anonymous) |
| 57 | 57 | * @return true|WP_Error True if within limits, WP_Error if exceeded |
| 58 | 58 | */ |
| 59 | - protected function check_rate_limit(string $endpoint_name, int $user_id = 0): bool|WP_Error { | |
| 59 | + protected function check_rate_limit(string $endpoint_name, int $user_id = 0) { | |
| 60 | 60 | // Get user identifier (IP for anonymous, user ID for authenticated) |
| 61 | 61 | $identifier = $user_id > 0 ? "user_{$user_id}" : $this->get_client_ip(); |
| 62 | 62 | |
| 63 | 63 | // Check minute-based rate limit |
| @@ -98,38 +98,31 @@ | ||
| 98 | 98 | * |
| 99 | 99 | * @return string Client IP address |
| 100 | 100 | */ |
| 101 | 101 | private function get_client_ip(): string { |
| 102 | - // Check for various headers that might contain the real IP | |
| 103 | - $headers = [ | |
| 104 | - 'HTTP_CF_CONNECTING_IP', // Cloudflare | |
| 105 | - 'HTTP_CLIENT_IP', // Proxy | |
| 106 | - 'HTTP_X_FORWARDED_FOR', // Load balancer/proxy | |
| 107 | - 'HTTP_X_FORWARDED', // Proxy | |
| 108 | - 'HTTP_X_CLUSTER_CLIENT_IP', // Cluster | |
| 109 | - 'HTTP_FORWARDED_FOR', // Proxy | |
| 110 | - 'HTTP_FORWARDED', // Proxy | |
| 111 | - 'REMOTE_ADDR' // Standard | |
| 112 | - ]; | |
| 102 | + // Forwarded headers are client-controlled: trusting them by default | |
| 103 | + // lets a caller mint a fresh rate-limit bucket per request. Only | |
| 104 | + // consult them when the site opts in because a trusted proxy/CDN | |
| 105 | + // sits in front and REMOTE_ADDR is the proxy, not the client. | |
| 106 | + $trusted_headers = apply_filters('thinkrank_trusted_ip_headers', []); | |
| 113 | 107 | |
| 114 | - foreach ($headers as $header) { | |
| 115 | - if (!empty($_SERVER[$header])) { | |
| 116 | - $ip = $_SERVER[$header]; | |
| 117 | - | |
| 118 | - // Handle comma-separated IPs (X-Forwarded-For can contain multiple IPs) | |
| 119 | - if (strpos($ip, ',') !== false) { | |
| 120 | - $ip = trim(explode(',', $ip)[0]); | |
| 121 | - } | |
| 122 | - | |
| 123 | - // Validate IP address | |
| 124 | - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { | |
| 125 | - return $ip; | |
| 126 | - } | |
| 108 | + foreach ((array) $trusted_headers as $header) { | |
| 109 | + if (empty($_SERVER[$header])) { | |
| 110 | + continue; | |
| 127 | 111 | } |
| 112 | + $ip = sanitize_text_field(wp_unslash($_SERVER[$header])); | |
| 113 | + | |
| 114 | + // X-Forwarded-For can contain a comma-separated chain. | |
| 115 | + if (strpos($ip, ',') !== false) { | |
| 116 | + $ip = trim(explode(',', $ip)[0]); | |
| 117 | + } | |
| 118 | + | |
| 119 | + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { | |
| 120 | + return $ip; | |
| 121 | + } | |
| 128 | 122 | } |
| 129 | 123 | |
| 130 | - // Fallback to REMOTE_ADDR even if it's a private IP | |
| 131 | - return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; | |
| 124 | + return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '0.0.0.0'; | |
| 132 | 125 | } |
| 133 | 126 | |
| 134 | 127 | /** |
| 135 | 128 | * Get rate limit status for a user/endpoint |