[Servatrice] Match trusted_sources by address, not substring (#7582)

trusted_sources was checked with QString::contains. A dual-stack listener
reports a local proxy as ::ffff:127.0.0.1, which never matches the default
"127.0.0.1,::1". Behind nginx the forwarded client IP header was then
ignored, every websocket user shared the proxy's address, and logins
failed with "too many concurrent connections" once max_users_per_address
was reached.

Add isTrustedSource(), which parses each entry as a QHostAddress and
compares exact addresses with IPv4-mapped addresses normalized, and use it
at all four call sites. The ban command also read server/trusted_sources
instead of the documented security/trusted_sources; it now uses the same
getter. Entries that are not IP addresses are logged at startup since they
no longer match anything.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zach H 2026-10-18 16:50:04 -04:00 • committed by GitHub
parent 4343544588
commit 46cdbac830
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 136 additions and 9 deletions

View file

@ -33,6 +33,7 @@
#include "serversocketinterface.h"
#include "settingscache.h"
#include "smtpclient.h"
#include "trusted_sources.h"
#include <QDateTime>
#include <QDebug>
@ -324,6 +325,14 @@ bool Servatrice::initServer()
qDebug() << "Maximum websocket user limit:" << getMaxWebSocketUserLimit();
}
// trusted_sources entries are matched as exact addresses; older configs may hold hostnames,
// prefixes or subnets that substring matching used to accept and that now never match.
for (const QString &entry : getTrustedSources().split(',', Qt::SkipEmptyParts)) {
if (QHostAddress(entry.trimmed()).isNull()) {
qWarning() << "security/trusted_sources entry is not an IP address and will be ignored:" << entry.trimmed();
}
}
qDebug() << "Accept registered users only:" << getRegOnlyServerEnabled();
qDebug() << "Registration enabled:" << getRegistrationEnabled();
if (getRegistrationEnabled()) {
@ -1324,6 +1333,11 @@ int Servatrice::getMaxUsersPerAddress() const
return settingsCache->value("security/max_users_per_address", 4).toInt();
}
QString Servatrice::getTrustedSources() const
{
return settingsCache->value("security/trusted_sources", "127.0.0.1,::1").toString();
}
int Servatrice::getMessageCountingInterval() const
{
return settingsCache->value("security/message_counting_interval", 10).toInt();
@ -1538,8 +1552,7 @@ 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.
const QString trustedSources = settingsCache->value("security/trusted_sources", "127.0.0.1,::1").toString();
return trustedSources.contains(ipAddress, Qt::CaseInsensitive);
return isTrustedSource(getTrustedSources(), ipAddress);
}
void Servatrice::clearFailedLogins(const QString &ipAddress, const QString &userName)