Brute-force lockout is keyed on the leftmost X-Forwarded-For value (incomplete fix for CVE-2025-52576)
Summary
Request::getIpAddress() honours proxy headers only when the peer is a configured trusted proxy — the control added for
CVE-2025-52576, and that gate is correct. It then takes the first entry of X-Forwarded-For. A reverse proxy
appends the real client on the right, so the leftmost entry is the value the client supplied. The IP-keyed CAPTCHA and
lockout are therefore stored under an attacker-chosen key.
Affected code
app/Core/Http/Request.php::getIpAddress():
foreach ($keys as $key) {
if ($this->getServerVariable($key) !== '') {
foreach (explode(',', $this->server[$key]) as $ipAddress) {
$ipAddress = trim($ipAddress);
if (filter_var($ipAddress, FILTER_VALIDATE_IP)) {
return $ipAddress; // leftmost wins
}
}
}
}Consumer — app/Model/CaptchaModel.php, keyed by that value:
$data[$ipAddress]['failed_login']++;
if ($data[$ipAddress]['failed_login'] >= BRUTEFORCE_CAPTCHA) { ... lock ... }Preconditions
TRUSTED_PROXY_HEADERS and TRUSTED_PROXY_NETWORKS configured. Both default to empty, so a default install is not
affected — this applies to the documented reverse-proxy deployment. No authentication required to exploit.
Steps to reproduce
Config used: TRUSTED_PROXY_HEADERS="HTTP_X_FORWARDED_FOR", TRUSTED_PROXY_NETWORKS="172.16.0.0/12,127.0.0.1/32".
- What the application receives behind the proxy (the proxy appends the real peer):
HTTP_X_FORWARDED_FOR = 203.0.113.77, 172.17.0.1,REMOTE_ADDR = 172.17.0.1 getIpAddress()resolves (probe inside the container with the real config loaded):XFF=1.2.3.4, 172.17.0.1 -> 1.2.3.4 XFF=8.8.8.8, 9.9.9.9, 172.17.0.1 -> 8.8.8.8 XFF=not-an-ip, 5.5.5.5 -> 5.5.5.5- Send failed logins with
X-Forwarded-For: 203.0.113.77(on both the GET and the POST). Thecaptcha_datasetting gains a new key created from the forged value:{"172.17.0.1": {"failed_login":5,"lock_expiration_date":1785001438}, "203.0.113.77": {"failed_login":2,"expiration_date":0}}
Rotating the header per attempt means no key reaches BRUTEFORCE_CAPTCHA / BRUTEFORCE_LOCKDOWN.
Impact
Restores the capability CVE-2025-52576 was filed to remove: unthrottled credential guessing and username enumeration
from a single host on proxy deployments. The per-username counter (users.nb_failed_login) is unaffected — this
defeats the IP-based half, which is the half that stops password-spraying across many usernames.
CVSS 3.1: 6.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N), CWE-290/CWE-307.
Fix
Walk the chain from the right, discarding hops you control, and take the first untrusted address:
$addresses = array_map('trim', explode(',', $this->server[$key]));
for ($i = count($addresses) - 1; $i >= 0; $i--) {
$addr = $addresses[$i];
if (! filter_var($addr, FILTER_VALIDATE_IP)) { continue; }
if ($this->isIpInNetworks($addr, $trustedProxyNetworks)) { continue; } // hop we control
return $addr; // first untrusted = client
}
return $this->getClientIpAddress();Alternatively prefer a single-valued header the proxy fully controls (X-Real-IP, which the official image already sets)
over the appendable list.
Verified against v1.2.52 and the current release v1.2.53 (2026-07-24) — file byte-identical in both. Reported as a regular bug per SECURITY.md.
Source: kanboard/kanboard