[Server] Parse the rightmost address from forwarded header lists (#7586)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

X-Forwarded-For carries a comma-separated list, and constructing a
QHostAddress from the raw header value returns a null address for it, so a
configured X-Forwarded-For or a multi-hop header never resolved and websocket
clients stayed on the proxy's address.

- Add parseForwardedAddress, returning the rightmost usable entry of a
  forwarded header list
- Use it for the websocket handshake instead of parsing the raw value
- Exempt loopback from per-address auth rate limits so a same-host proxy with a
  customized trusted_sources list cannot lock every user out
- Add forwarded_address_test

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
Zach H 2026-10-18 16:53:55 -04:00 • committed by GitHub
parent 46cdbac830
commit ea6af0048c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 100 additions and 7 deletions

View file

@ -0,0 +1,21 @@
#include "forwarded_address.h"
#include <QString>
QHostAddress parseForwardedAddress(const QByteArray &headerValue)
{
const QList<QByteArray> entries = headerValue.split(',');
for (auto it = entries.crbegin(); it != entries.crend(); ++it) {
const QByteArray entry = it->trimmed();
if (entry.isEmpty()) {
continue;
}
const QHostAddress parsed(QString::fromLatin1(entry));
if (!parsed.isNull()) {
return parsed;
}
}
return {};
}

View file

@ -0,0 +1,18 @@
#ifndef FORWARDED_ADDRESS_H
#define FORWARDED_ADDRESS_H
#include <QByteArray>
#include <QHostAddress>
/**
* @brief Parses the real client address out of a forwarded header value (X-Real-IP, X-Forwarded-For).
*
* A comma-separated list resolves to its rightmost usable entry. That entry was appended by the
* proxy closest to the server and is the only one a direct client cannot choose, since the earlier
* entries are passed through verbatim from whatever the client sent.
*
* @return the parsed address, or a null QHostAddress when the value holds none.
*/
QHostAddress parseForwardedAddress(const QByteArray &headerValue);
#endif

View file

@ -1551,8 +1551,11 @@ bool Servatrice::isRateLimitExempt(const QString &ipAddress) const
{
// Mirrors the max_users_per_address bypass in initSession: trusted sources
// (e.g. 127.0.0.1 for Webatrice behind local nginx, where every websocket
// client shares one address) are not subject to per-address limits.
return isTrustedSource(getTrustedSources(), ipAddress);
// client shares one address) are not subject to per-address limits. Loopback
// is always exempt, so a same-host proxy whose forwarded address could not be
// resolved cannot lock every user out together when trusted_sources is
// customized.
return QHostAddress(ipAddress).isLoopback() || isTrustedSource(getTrustedSources(), ipAddress);
}
void Servatrice::clearFailedLogins(const QString &ipAddress, const QString &userName)

View file

@ -22,6 +22,7 @@
#include "deck_tag_serialization.h"
#include "email_parser.h"
#include "forwarded_address.h"
#include "libcockatrice/protocol/pb/admin_commands.pb.h"
#include "libcockatrice/protocol/pb/developer_commands.pb.h"
#include "libcockatrice/protocol/pb/moderator_commands.pb.h"
@ -4560,8 +4561,7 @@ void WebsocketServerSocketInterface::initConnection(void *_socket)
// Only trust the forwarded header if the connection itself comes from a trusted proxy,
// otherwise a client could spoof its address to bypass bans and rate limits.
if (isTrustedSource(servatrice->getTrustedSources(), socket->peerAddress())) {
QString header(socket->request().rawHeader(websocketIPHeader));
QHostAddress parsed(header);
const QHostAddress parsed = parseForwardedAddress(socket->request().rawHeader(websocketIPHeader));
if (!parsed.isNull()) {
address = parsed;
}