diff options
Diffstat (limited to 'src/browser/BrowserService.cpp')
-rw-r--r-- | src/browser/BrowserService.cpp | 285 |
1 files changed, 144 insertions, 141 deletions
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index eb752996c..9cb2e2729 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -371,8 +371,8 @@ QString BrowserService::getKey(const QString& id) } QJsonArray BrowserService::findMatchingEntries(const QString& dbid, - const QString& url, - const QString& submitUrl, + const QString& siteUrlStr, + const QString& formUrlStr, const QString& realm, const StringPairList& keyList, const bool httpAuth) @@ -380,13 +380,13 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, Q_UNUSED(dbid); const bool alwaysAllowAccess = browserSettings()->alwaysAllowAccess(); const bool ignoreHttpAuth = browserSettings()->httpAuthPermission(); - const QString host = QUrl(url).host(); - const QString submitHost = QUrl(submitUrl).host(); + const QString siteHost = QUrl(siteUrlStr).host(); + const QString formHost = QUrl(formUrlStr).host(); // Check entries for authorization QList<Entry*> pwEntriesToConfirm; QList<Entry*> pwEntries; - for (auto* entry : searchEntries(url, submitUrl, keyList)) { + for (auto* entry : searchEntries(siteUrlStr, formUrlStr, keyList)) { if (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY) && entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR) { continue; @@ -403,7 +403,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, continue; } - switch (checkAccess(entry, host, submitHost, realm)) { + switch (checkAccess(entry, siteHost, formHost, realm)) { case Denied: continue; @@ -422,7 +422,8 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, } // Confirm entries - QList<Entry*> selectedEntriesToConfirm = confirmEntries(pwEntriesToConfirm, url, host, submitHost, realm, httpAuth); + QList<Entry*> selectedEntriesToConfirm = + confirmEntries(pwEntriesToConfirm, siteUrlStr, siteHost, formHost, realm, httpAuth); if (!selectedEntriesToConfirm.isEmpty()) { pwEntries.append(selectedEntriesToConfirm); } @@ -437,7 +438,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, } // Sort results - pwEntries = sortEntries(pwEntries, host, submitUrl, url); + pwEntries = sortEntries(pwEntries, siteUrlStr, formUrlStr); // Fill the list QJsonArray result; @@ -451,8 +452,8 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, void BrowserService::addEntry(const QString& dbid, const QString& login, const QString& password, - const QString& url, - const QString& submitUrl, + const QString& siteUrlStr, + const QString& formUrlStr, const QString& realm, const QString& group, const QString& groupUuid, @@ -467,8 +468,8 @@ void BrowserService::addEntry(const QString& dbid, auto* entry = new Entry(); entry->setUuid(QUuid::createUuid()); - entry->setTitle(QUrl(url).host()); - entry->setUrl(url); + entry->setTitle(QUrl(siteUrlStr).host()); + entry->setUrl(siteUrlStr); entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); entry->setUsername(login); entry->setPassword(password); @@ -487,8 +488,8 @@ void BrowserService::addEntry(const QString& dbid, entry->setGroup(getDefaultEntryGroup(db)); } - const QString host = QUrl(url).host(); - const QString submitHost = QUrl(submitUrl).host(); + const QString host = QUrl(siteUrlStr).host(); + const QString submitHost = QUrl(formUrlStr).host(); BrowserEntryConfig config; config.allow(host); @@ -505,8 +506,8 @@ bool BrowserService::updateEntry(const QString& dbid, const QString& uuid, const QString& login, const QString& password, - const QString& url, - const QString& submitUrl) + const QString& siteUrlStr, + const QString& formUrlStr) { // TODO: select database based on this key id Q_UNUSED(dbid); @@ -518,7 +519,7 @@ bool BrowserService::updateEntry(const QString& dbid, Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid)); if (!entry) { // If entry is not found for update, add a new one to the selected database - addEntry(dbid, login, password, url, submitUrl, "", "", "", db); + addEntry(dbid, login, password, siteUrlStr, formUrlStr, "", "", "", db); return true; } @@ -547,7 +548,7 @@ bool BrowserService::updateEntry(const QString& dbid, dialogResult = MessageBox::question( nullptr, tr("KeePassXC: Update Entry"), - tr("Do you want to update the information in %1 - %2?").arg(QUrl(url).host(), username), + tr("Do you want to update the information in %1 - %2?").arg(QUrl(siteUrlStr).host(), username), MessageBox::Save | MessageBox::Cancel, MessageBox::Cancel, MessageBox::Raise); @@ -570,7 +571,7 @@ bool BrowserService::updateEntry(const QString& dbid, } QList<Entry*> -BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& url, const QString& submitUrl) +BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& siteUrlStr, const QString& formUrlStr) { QList<Entry*> entries; auto* rootGroup = db->rootGroup(); @@ -590,25 +591,29 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& // Search for additional URL's starting with KP2A_URL for (const auto& key : entry->attributes()->keys()) { - if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), url, submitUrl) + if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), siteUrlStr, formUrlStr) && !entries.contains(entry)) { entries.append(entry); continue; } } - if (!handleURL(entry->url(), url, submitUrl)) { + if (!handleURL(entry->url(), siteUrlStr, formUrlStr)) { continue; } - entries.append(entry); + // Additional URL check may have already inserted the entry to the list + if (!entries.contains(entry)) { + entries.append(entry); + } } } return entries; } -QList<Entry*> BrowserService::searchEntries(const QString& url, const QString& submitUrl, const StringPairList& keyList) +QList<Entry*> +BrowserService::searchEntries(const QString& siteUrlStr, const QString& formUrlStr, const StringPairList& keyList) { // Check if database is connected with KeePassXC-Browser auto databaseConnected = [&](const QSharedPointer<Database>& db) { @@ -638,11 +643,11 @@ QList<Entry*> BrowserService::searchEntries(const QString& url, const QString& s } // Search entries matching the hostname - QString hostname = QUrl(url).host(); + QString hostname = QUrl(siteUrlStr).host(); QList<Entry*> entries; do { for (const auto& db : databases) { - entries << searchEntries(db, url, submitUrl); + entries << searchEntries(db, siteUrlStr, formUrlStr); } } while (entries.isEmpty() && removeFirstDomain(hostname)); @@ -722,47 +727,30 @@ void BrowserService::convertAttributesToCustomData(QSharedPointer<Database> db) } } -QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, - const QString& host, - const QString& entryUrl, - const QString& fullUrl) +QList<Entry*> +BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& siteUrlStr, const QString& formUrlStr) { - QUrl url(entryUrl); - if (url.scheme().isEmpty()) { - url.setScheme("https"); - } - - const QString submitUrl = url.toString(QUrl::StripTrailingSlash); - const QString baseSubmitUrl = - url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); - // Build map of prioritized entries QMultiMap<int, Entry*> priorities; for (auto* entry : pwEntries) { - priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl, fullUrl), entry); + priorities.insert(sortPriority(getEntryURLs(entry), siteUrlStr, formUrlStr), entry); } + auto keys = priorities.uniqueKeys(); + std::sort(keys.begin(), keys.end(), [](int l, int r) { return l > r; }); + QList<Entry*> results; - QString field = browserSettings()->sortByTitle() ? "Title" : "UserName"; - for (int i = 100; i >= 0; i -= 5) { - if (priorities.count(i) > 0) { - // Sort same priority entries by Title or UserName - auto entries = priorities.values(i); - std::sort(entries.begin(), entries.end(), [&field](Entry* left, Entry* right) { - return (QString::localeAwareCompare(left->attributes()->value(field), right->attributes()->value(field)) - < 0) - || ((QString::localeAwareCompare(left->attributes()->value(field), - right->attributes()->value(field)) - == 0) - && (QString::localeAwareCompare(left->attributes()->value("UserName"), - right->attributes()->value("UserName")) - < 0)); - }); - results << entries; - if (browserSettings()->bestMatchOnly() && !pwEntries.isEmpty()) { - // Early out once we find the highest batch of matches - break; - } + auto sortField = browserSettings()->sortByTitle() ? EntryAttributes::TitleKey : EntryAttributes::UserNameKey; + for (auto key : keys) { + // Sort same priority entries by Title or UserName + auto entries = priorities.values(key); + std::sort(entries.begin(), entries.end(), [&sortField](Entry* left, Entry* right) { + return QString::localeAwareCompare(left->attribute(sortField), right->attribute(sortField)); + }); + results << entries; + if (browserSettings()->bestMatchOnly() && !results.isEmpty()) { + // Early out once we find the highest batch of matches + break; } } @@ -770,9 +758,9 @@ QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, } QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm, - const QString& url, - const QString& host, - const QString& submitHost, + const QString& siteUrlStr, + const QString& siteHost, + const QString& formUrlStr, const QString& realm, const bool httpAuth) { @@ -790,9 +778,9 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm, auto entry = pwEntriesToConfirm[item->row()]; BrowserEntryConfig config; config.load(entry); - config.deny(host); - if (!submitHost.isEmpty() && host != submitHost) { - config.deny(submitHost); + config.deny(siteHost); + if (!formUrlStr.isEmpty() && siteHost != formUrlStr) { + config.deny(formUrlStr); } if (!realm.isEmpty()) { config.setRealm(realm); @@ -800,7 +788,7 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm, config.save(entry); }); - accessControlDialog.setItems(pwEntriesToConfirm, url, httpAuth); + accessControlDialog.setItems(pwEntriesToConfirm, siteUrlStr, httpAuth); QList<Entry*> allowedEntries; if (accessControlDialog.exec() == QDialog::Accepted) { @@ -810,9 +798,9 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm, if (accessControlDialog.remember()) { BrowserEntryConfig config; config.load(entry); - config.allow(host); - if (!submitHost.isEmpty() && host != submitHost) { - config.allow(submitHost); + config.allow(siteHost); + if (!formUrlStr.isEmpty() && siteHost != formUrlStr) { + config.allow(formUrlStr); } if (!realm.isEmpty()) { config.setRealm(realm); @@ -871,7 +859,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry) } BrowserService::Access -BrowserService::checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm) +BrowserService::checkAccess(const Entry* entry, const QString& siteHost, const QString& formHost, const QString& realm) { if (entry->isExpired()) { return browserSettings()->allowExpiredCredentials() ? Allowed : Denied; @@ -881,10 +869,10 @@ BrowserService::checkAccess(const Entry* entry, const QString& host, const QStri if (!config.load(entry)) { return Unknown; } - if ((config.isAllowed(host)) && (submitHost.isEmpty() || config.isAllowed(submitHost))) { + if ((config.isAllowed(siteHost)) && (formHost.isEmpty() || config.isAllowed(formHost))) { return Allowed; } - if ((config.isDenied(host)) || (!submitHost.isEmpty() && config.isDenied(submitHost))) { + if ((config.isDenied(siteHost)) || (!formHost.isEmpty() && config.isDenied(formHost))) { return Denied; } if (!realm.isEmpty() && config.realm() != realm) { @@ -919,66 +907,72 @@ Group* BrowserService::getDefaultEntryGroup(const QSharedPointer<Database>& sele return group; } -int BrowserService::sortPriority(const Entry* entry, - const QString& host, - const QString& submitUrl, - const QString& baseSubmitUrl, - const QString& fullUrl) const +// Returns the maximum sort priority given a set of match urls and the +// extension provided site and form url. +int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr) { - QUrl url(entry->url()); - if (url.scheme().isEmpty()) { - url.setScheme("https"); - } + QList<int> priorityList; + // NOTE: QUrl::matches is utterly broken in Qt < 5.11, so we work around that + // by removing parts of the url that we don't match and direct matching others + const auto stdOpts = QUrl::RemoveFragment | QUrl::RemoveUserInfo; + const auto siteUrl = QUrl(siteUrlStr).adjusted(stdOpts); + const auto formUrl = QUrl(formUrlStr).adjusted(stdOpts); + + auto getPriority = [&](const QString& givenUrl) { + auto url = QUrl::fromUserInput(givenUrl).adjusted(stdOpts); + + // Default to https scheme if undefined + if (url.scheme().isEmpty() || !givenUrl.contains("://")) { + url.setScheme("https"); + } - // Add the empty path to the URL if it's missing - if (url.path().isEmpty() && !url.hasFragment() && !url.hasQuery()) { - url.setPath("/"); - } + // Add the empty path to the URL if it's missing. + // URL's from the extension always have a path set, entry URL's can be without. + if (url.path().isEmpty() && !url.hasFragment() && !url.hasQuery()) { + url.setPath("/"); + } + + // Reject invalid urls and hosts, except 'localhost', and scheme mismatch + if (!url.isValid() || (!url.host().contains(".") && url.host() != "localhost") + || url.scheme() != siteUrl.scheme()) { + return 0; + } + + // Exact match with site url or form url + if (url.matches(siteUrl, QUrl::None) || url.matches(formUrl, QUrl::None)) { + return 100; + } + + // Exact match without the query string + if (url.matches(siteUrl, QUrl::RemoveQuery) || url.matches(formUrl, QUrl::RemoveQuery)) { + return 90; + } + + // Match without path (ie, FQDN match), form url prioritizes lower than site url + if (url.host() == siteUrl.host()) { + return 80; + } + if (url.host() == formUrl.host()) { + return 70; + } - const QString entryURL = url.toString(QUrl::StripTrailingSlash); - const QString baseEntryURL = - url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); + // Site/form url ends with given url (subdomain mismatch) + if (siteUrl.host().endsWith(url.host())) { + return 60; + } + if (formUrl.host().endsWith(url.host())) { + return 50; + } - if (!url.host().contains(".") && url.host() != "localhost") { + // No valid match found return 0; + }; + + for (const auto& entryUrl : urls) { + priorityList << getPriority(entryUrl); } - if (fullUrl == entryURL) { - return 100; - } - if (submitUrl == entryURL) { - return 95; - } - if (submitUrl.startsWith(entryURL) && entryURL != host && baseSubmitUrl != entryURL) { - return 90; - } - if (submitUrl.startsWith(baseEntryURL) && entryURL != host && baseSubmitUrl != baseEntryURL) { - return 80; - } - if (entryURL == host) { - return 70; - } - if (entryURL == baseSubmitUrl) { - return 60; - } - if (entryURL.startsWith(submitUrl)) { - return 50; - } - if (entryURL.startsWith(baseSubmitUrl) && baseSubmitUrl != host) { - return 40; - } - if (submitUrl.startsWith(entryURL)) { - return 30; - } - if (submitUrl.startsWith(baseEntryURL)) { - return 20; - } - if (entryURL.startsWith(host)) { - return 10; - } - if (host.startsWith(entryURL)) { - return 5; - } - return 0; + + return *std::max_element(priorityList.begin(), priorityList.end()); } bool BrowserService::schemeFound(const QString& url) @@ -1004,7 +998,7 @@ bool BrowserService::removeFirstDomain(QString& hostname) return false; } -bool BrowserService::handleURL(const QString& entryUrl, const QString& url, const QString& submitUrl) +bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr) { if (entryUrl.isEmpty()) { return false; @@ -1022,8 +1016,8 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& url, cons } // Make a direct compare if a local file is used - if (url.contains("file://")) { - return entryUrl == submitUrl; + if (siteUrlStr.contains("file://")) { + return entryUrl == formUrlStr; } // URL host validation fails @@ -1032,7 +1026,7 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& url, cons } // Match port, if used - QUrl siteQUrl(url); + QUrl siteQUrl(siteUrlStr); if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) { return false; } @@ -1056,17 +1050,7 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& url, cons // Match the subdomains with the limited wildcard if (siteQUrl.host().endsWith(entryQUrl.host())) { - if (!browserSettings()->bestMatchOnly()) { - return true; - } - - // Match the exact subdomain and path, or start of the path when entry's path is longer than plain "/" - if (siteQUrl.host() == entryQUrl.host()) { - if (siteQUrl.path() == entryQUrl.path() - || (entryQUrl.path().size() > 1 && siteQUrl.path().startsWith(entryQUrl.path()))) { - return true; - } - } + return true; } return false; @@ -1182,6 +1166,10 @@ bool BrowserService::checkLegacySettings(QSharedPointer<Database> db) bool legacySettingsFound = false; QList<Entry*> entries = db->rootGroup()->entriesRecursive(); for (const auto& e : entries) { + if (e->isRecycled()) { + continue; + } + if ((e->attributes()->contains(KEEPASSHTTP_NAME) || e->attributes()->contains(KEEPASSXCBROWSER_NAME)) || (e->title() == KEEPASSHTTP_NAME || e->title().contains(KEEPASSXCBROWSER_NAME, Qt::CaseInsensitive))) { legacySettingsFound = true; @@ -1212,6 +1200,21 @@ bool BrowserService::checkLegacySettings(QSharedPointer<Database> db) return dialogResult == MessageBox::Yes; } +QStringList BrowserService::getEntryURLs(const Entry* entry) +{ + QStringList urlList; + urlList << entry->url(); + + // Handle additional URL's + for (const auto& key : entry->attributes()->keys()) { + if (key.startsWith(ADDITIONAL_URL)) { + urlList << entry->attributes()->value(key); + } + } + + return urlList; +} + void BrowserService::hideWindow() const { if (m_prevWindowState == WindowState::Minimized) { |