Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/gui/accountmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ constexpr auto caCertsKeyC = "CaCertificates";
constexpr auto accountsC = "Accounts";
constexpr auto versionC = "version";
constexpr auto serverVersionC = "serverVersion";
constexpr auto trustedRedirectHostsC = "trustedRedirectHosts";
constexpr auto serverColorC = "serverColor";
constexpr auto serverTextColorC = "serverTextColor";
constexpr auto skipE2eeMetadataChecksumValidationC = "skipE2eeMetadataChecksumValidation";
Expand Down Expand Up @@ -645,6 +646,9 @@ AccountPtr AccountManager::loadAccountHelper(QSettings &settings)
qCInfo(lcAccountManager) << "Account for" << acc->url() << "using auth type" << authType;

acc->_serverVersion = settings.value(QLatin1String(serverVersionC)).toString();
if (const auto trustedRedirectHosts = settings.value(QLatin1String(trustedRedirectHostsC)).toStringList(); !trustedRedirectHosts.isEmpty()) {
acc->setTrustedRedirectHosts(trustedRedirectHosts);
}
acc->_serverColor = settings.value(QLatin1String(serverColorC)).value<QColor>();
acc->_serverTextColor = settings.value(QLatin1String(serverTextColorC)).value<QColor>();
acc->_serverHasValidSubscription = settings.value(QLatin1String(serverHasValidSubscriptionC), false).value<bool>();
Expand Down
48 changes: 46 additions & 2 deletions src/libsync/abstractnetworkjob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,50 @@ namespace OCC {

Q_LOGGING_CATEGORY(lcNetworkJob, "nextcloud.sync.networkjob", QtInfoMsg)

namespace {

// Registrable domain (eTLD+1) of url, using Qt's public-suffix list.
// Falls back to the bare host for IP addresses and intranet names that have
// no public suffix, so those only ever match themselves exactly.
QString registrableDomain(const QUrl &url)
{
const auto host = url.host();
const auto tld = url.topLevelDomain();
if (tld.isEmpty() || !host.endsWith(tld)) {
return host;
}
auto label = host.left(host.size() - tld.size());
const auto lastDot = label.lastIndexOf(QLatin1Char('.'));
return (lastDot < 0 ? label : label.mid(lastDot + 1)) + tld;
}

// Whether the Authorization header / credentials may follow a redirect from
// origin to target without leaking them to an untrusted host.
bool redirectKeepsCredentials(const QUrl &origin, const QUrl &target,
const QUrl &accountUrl, const QStringList &trustedHosts)
{
// Never carry credentials across a scheme change (e.g. HTTPS -> HTTP).
if (origin.scheme() != target.scheme()) {
return false;
}
// Same host:port as the immediate origin.
if (origin.host() == target.host() && origin.port() == target.port()) {
return true;
}
// Same registrable domain as the account's own server. Covers SSO /
// reverse-proxy setups such as auth.example.com <-> cloud.example.com.
if (accountUrl.scheme() == target.scheme()) {
const auto accountDomain = registrableDomain(accountUrl);
if (!accountDomain.isEmpty() && accountDomain == registrableDomain(target)) {
return true;
}
}
// Explicitly trusted host (e.g. a third-party identity provider).
return trustedHosts.contains(target.host(), Qt::CaseInsensitive);
}

} // anonymous namespace

// If not set, it is overwritten by the Application constructor with the value from the config
int AbstractNetworkJob::httpTimeout = qEnvironmentVariableIntValue("OWNCLOUD_TIMEOUT");
bool AbstractNetworkJob::enableTimeout = true;
Expand Down Expand Up @@ -276,8 +320,8 @@ void AbstractNetworkJob::slotFinished()

auto request = reply()->request();

if (!(requestedUrl.host() == redirectUrl.host() && requestedUrl.port() == redirectUrl.port())) {
qCWarning(lcNetworkJob).nospace() << "redirect target mismatches origin, removing credentials"
if (!redirectKeepsCredentials(requestedUrl, redirectUrl, _account->url(), _account->trustedRedirectHosts())) {
qCWarning(lcNetworkJob).nospace() << "redirect target is not trusted, removing credentials"
<< " origin=" << requestedUrl.host() << ":" << requestedUrl.port()
<< " target=" << redirectUrl.host() << ":" << redirectUrl.port();

Expand Down
10 changes: 10 additions & 0 deletions src/libsync/account.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,16 @@ void Account::setUserVisibleHost(const QString &host)
_userVisibleUrl.setHost(host);
}

QStringList Account::trustedRedirectHosts() const
{
return _settingsMap.value(QStringLiteral("trustedRedirectHosts")).toStringList();
}

void Account::setTrustedRedirectHosts(const QStringList &hosts)
{
_settingsMap.insert(QStringLiteral("trustedRedirectHosts"), hosts);
}

QVariant Account::credentialSetting(const QString &key) const
{
if (_credentials) {
Expand Down
8 changes: 8 additions & 0 deletions src/libsync/account.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "updatechannel.h"

#include <QByteArray>
#include <QStringList>
#include <QUrl>
#include <QNetworkCookie>
#include <QNetworkProxy>
Expand Down Expand Up @@ -243,6 +244,13 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject
void setApprovedCerts(const QList<QSslCertificate> certs);
void addApprovedCerts(const QList<QSslCertificate> certs);

/** Additional hosts to which credentials may still be sent when an HTTP
* redirect targets a host other than the account's own. Redirects within
* the account's own registrable domain are always trusted; this list
* covers cross-domain cases such as a third-party SSO / identity provider. */
[[nodiscard]] QStringList trustedRedirectHosts() const;
void setTrustedRedirectHosts(const QStringList &hosts);

// Usually when a user explicitly rejects a certificate we don't
// ask again. After this call, a dialog will again be shown when
// the next unknown certificate is encountered.
Expand Down