Fix: Recover proxied client IPs and fit audit filters on phones
The MFC web server now resolves the real client address from the WebSocket upgrade request when the TCP peer is loopback, so audit records and login rate limiting stop reporting the reverse proxy's 127.0.0.1. X-Forwarded-For is preferred over X-Real-IP because its last hop is the address the local proxy appended; a client-supplied header list cannot forge it. Forwarded headers are ignored for non-loopback peers so a client connecting directly over the internet cannot spoof its identity or rotate addresses to bypass throttling. FRP deployments can keep using the existing Proxy Protocol v2 support instead. Collapse the audit log search conditions behind a toolbar toggle that starts closed and switches to an accent-colored "Hide filters" state while the panel is open. On portrait phones the panel uses one control per row, and From/To move their border and padding onto a wrapper so the iOS WebKit datetime-local width miscalculation cannot overflow the viewport. Co-Authored-By: deepseek-flash
This commit is contained in:
1 parent
ca671d6453
commit
bbb3d8c2fa
2 files changed
+156
-5
No files matched your search
@@ -349,6 +349,75 @@ public:
|
||||
void onHttp(HttpHandler handler) { m_httpHandler = handler; }
|
||||
|
||||
private:
|
||||
// ---- Real client IP behind a reverse proxy ----
|
||||
// A same-host nginx/Caddy connects from 127.0.0.1 and reports the original
|
||||
// peer in X-Real-IP / X-Forwarded-For. Only trust those headers when the
|
||||
// TCP peer is loopback: otherwise any internet client could forge its
|
||||
// address in the audit log and rotate it to evade login throttling.
|
||||
static std::string trimHttpValue(const std::string& s) {
|
||||
size_t b = s.find_first_not_of(" \t");
|
||||
if (b == std::string::npos) return "";
|
||||
size_t e = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(b, e - b + 1);
|
||||
}
|
||||
|
||||
static std::string asciiLower(const std::string& s) {
|
||||
std::string out = s;
|
||||
for (char& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Case-insensitive lookup of one header in the raw request. The request
|
||||
// line is first, so matching "\r\n<name>:" cannot hit the request target.
|
||||
static std::string httpHeader(const std::string& request, const char* name) {
|
||||
const std::string needle = "\r\n" + asciiLower(name) + ":";
|
||||
const std::string hay = asciiLower(request);
|
||||
size_t pos = hay.find(needle);
|
||||
if (pos == std::string::npos) return "";
|
||||
size_t vstart = pos + needle.size();
|
||||
size_t vend = hay.find("\r\n", vstart);
|
||||
return trimHttpValue(request.substr(vstart,
|
||||
vend == std::string::npos ? std::string::npos : vend - vstart));
|
||||
}
|
||||
|
||||
// Accept only a bare IPv4/IPv6 literal; forwarded headers are attacker
|
||||
// controlled even when the proxy is trusted, so no hostnames/ports.
|
||||
static std::string normalizeIPLiteral(const std::string& value) {
|
||||
std::string s = trimHttpValue(value);
|
||||
if (s.empty() || s.size() > 63) return "";
|
||||
struct in_addr v4;
|
||||
if (inet_pton(AF_INET, s.c_str(), &v4) == 1) return s;
|
||||
struct in6_addr v6;
|
||||
if (inet_pton(AF_INET6, s.c_str(), &v6) == 1) return s;
|
||||
return "";
|
||||
}
|
||||
|
||||
static std::string forwardedClientIP(const std::string& request) {
|
||||
// X-Forwarded-For is "client, proxy1, ..." with the oldest hop first.
|
||||
// Our own proxy appends the peer it saw, so the last valid entry is
|
||||
// the one it actually observed: a client-supplied list cannot forge
|
||||
// it. Prefer this over X-Real-IP, which is only trustworthy when the
|
||||
// proxy explicitly overwrites it (some configs pass it through).
|
||||
std::string xff = httpHeader(request, "x-forwarded-for");
|
||||
size_t end = xff.size();
|
||||
while (end > 0) {
|
||||
size_t comma = xff.rfind(',', end - 1);
|
||||
size_t start = comma == std::string::npos ? 0 : comma + 1;
|
||||
std::string ip = normalizeIPLiteral(xff.substr(start, end - start));
|
||||
if (!ip.empty()) return ip;
|
||||
if (comma == std::string::npos) break;
|
||||
end = comma;
|
||||
}
|
||||
return normalizeIPLiteral(httpHeader(request, "x-real-ip"));
|
||||
}
|
||||
|
||||
static bool isLoopbackPeer(const std::string& ip) {
|
||||
return ip.rfind("127.", 0) == 0 || ip == "::1" ||
|
||||
ip == "::ffff:127.0.0.1";
|
||||
}
|
||||
|
||||
// Parse Proxy Protocol v2 header and extract real client IP
|
||||
bool parseProxyProtocolV2(SOCKET sock, std::string& clientIP) {
|
||||
// PP2 signature (12 bytes)
|
||||
@@ -448,6 +517,13 @@ private:
|
||||
if (request.find("\r\n\r\n") != std::string::npos) break;
|
||||
}
|
||||
|
||||
// Behind a same-host reverse proxy the TCP peer is 127.0.0.1; recover
|
||||
// the real client address the proxy recorded in the handshake.
|
||||
if (isLoopbackPeer(clientIP)) {
|
||||
std::string forwarded = forwardedClientIP(request);
|
||||
if (!forwarded.empty()) clientIP = forwarded;
|
||||
}
|
||||
|
||||
// Check if WebSocket upgrade
|
||||
if (request.find("Upgrade: websocket") == std::string::npos) {
|
||||
// Regular HTTP request - extract path
|
||||
|
||||
Reference in new issue
Block a user