فهرست منبع

fix: return client IPv6 address via cloudflared (#757)

* fix: return client IPv6 address via cloudflared

The cloudflared reverse proxy populates the X-Forwarded-For header for origin IPv4 addresses, however origin IPv6 addresses are added in a different header: Cf-Connecting-Ipv6. This updates the getIP.php mechanism to retrieve the value of this header and to prefer it over other client IP headers (in both cases only if the Cf-Connecting-Ipv6 header exists and is not empty).

* fix: Validate and normalise IP addresses from request headers

getClientIp() used HTTP_CF_CONNECTING_IPV6 and other headers verbatim, allowing malformed values to reach ISP lookups and the offline DB.

Add normalizeCandidateIp() helper that trims whitespace, extracts the first comma-separated token, and validates via filter_var(). Require FILTER_FLAG_IPV6 for the CF header and fall through to the next source on failure.

Written with assistance from OpenCode using Claude Opus 4.6.
Matthew Kobayashi 1 ماه پیش
والد
کامیت
f1f48ae53e
1فایلهای تغییر یافته به همراه48 افزوده شده و 12 حذف شده
  1. 48 12
      backend/getIP_util.php

+ 48 - 12
backend/getIP_util.php

@@ -1,19 +1,55 @@
 <?php
 
 /**
- * @return string
+ * Normalize and validate an IP address candidate from a request header.
+ *
+ * Trims whitespace, takes the first comma-separated token (for XFF-like
+ * headers that may contain a chain of addresses), and validates the result
+ * with filter_var().
+ *
+ * @param string $raw       Raw header value.
+ * @param int    $extraFlags Additional FILTER_FLAG_* flags (e.g. FILTER_FLAG_IPV6).
+ *
+ * @return string|false The validated IP string, or false on failure.
  */
-function getClientIp() {
-    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
-        $ip = $_SERVER['HTTP_CLIENT_IP'];
-    } elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
-        $ip = $_SERVER['HTTP_X_REAL_IP'];
-    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
-        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
-        $ip = preg_replace('/,.*/', '', $ip); # hosts are comma-separated, client is first
-    } else {
-        $ip = $_SERVER['REMOTE_ADDR'];
+function normalizeCandidateIp($raw, $extraFlags = 0)
+{
+    $ip = trim($raw);
+    // For XFF-like values, take the first address before a comma.
+    if (($pos = strpos($ip, ',')) !== false) {
+        $ip = trim(substr($ip, 0, $pos));
+    }
+    if ($ip === '') {
+        return false;
     }
+    return filter_var($ip, FILTER_VALIDATE_IP, $extraFlags);
+}
 
-    return preg_replace('/^::ffff:/', '', $ip);
+/**
+ * @return string
+ */
+function getClientIp()
+{
+    // Cloudflare IPv6 header — must be a valid IPv6 address.
+    if (!empty($_SERVER['HTTP_CF_CONNECTING_IPV6'])) {
+        $ip = normalizeCandidateIp($_SERVER['HTTP_CF_CONNECTING_IPV6'], FILTER_FLAG_IPV6);
+        if ($ip !== false) {
+            return preg_replace('/^::ffff:/', '', $ip);
+        }
+    }
+    // Other forwarding / proxy headers — accept any valid IP.
+    foreach (['HTTP_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'] as $header) {
+        if (!empty($_SERVER[$header])) {
+            $ip = normalizeCandidateIp($_SERVER[$header]);
+            if ($ip !== false) {
+                return preg_replace('/^::ffff:/', '', $ip);
+            }
+        }
+    }
+    // Fallback: REMOTE_ADDR is set by the web server and is always a single IP.
+    $ip = normalizeCandidateIp($_SERVER['REMOTE_ADDR'] ?? '');
+    if ($ip !== false) {
+        return preg_replace('/^::ffff:/', '', $ip);
+    }
+    return $_SERVER['REMOTE_ADDR'] ?? '';
 }