Add support for URL wildcards and exact URL (#9835)

* Add support for URL wildcards with Additional URL feature

* Only check TLD if wildcard is used

* Avoid using network function in no-feature build

---------

Co-authored-by: varjolintu <sami.vanttinen@ahmala.org>
Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
Sami Vänttinen 2025-02-10 03:03:15 +02:00 committed by GitHub
parent 51e8c042af
commit 9ba6ada266
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 317 additions and 36 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> * Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand * Copyright (C) 2013 Francois Ferrand
* *
@ -51,6 +51,7 @@
#include <QLocalSocket> #include <QLocalSocket>
#include <QLocale> #include <QLocale>
#include <QProgressDialog> #include <QProgressDialog>
#include <QStringView>
#include <QUrl> #include <QUrl>
const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings"); const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
@ -1375,9 +1376,15 @@ bool BrowserService::shouldIncludeEntry(Entry* entry,
return url.endsWith("by-path/" + entry->path()); return url.endsWith("by-path/" + entry->path());
} }
const auto allEntryUrls = entry->getAllUrls(); // Handle the entry URL
for (const auto& entryUrl : allEntryUrls) { if (handleURL(entry->resolveUrl(), url, submitUrl, omitWwwSubdomain)) {
if (handleURL(entryUrl, url, submitUrl, omitWwwSubdomain)) { return true;
}
// Handle additional URLs
const auto additionalUrls = entry->getAdditionalUrls();
for (const auto& additionalUrl : additionalUrls) {
if (handleURL(additionalUrl, url, submitUrl, omitWwwSubdomain, true)) {
return true; return true;
} }
} }
@ -1465,17 +1472,35 @@ QJsonObject BrowserService::getPasskeyError(int errorCode) const
bool BrowserService::handleURL(const QString& entryUrl, bool BrowserService::handleURL(const QString& entryUrl,
const QString& siteUrl, const QString& siteUrl,
const QString& formUrl, const QString& formUrl,
const bool omitWwwSubdomain) const bool omitWwwSubdomain,
const bool allowWildcards)
{ {
if (entryUrl.isEmpty()) { if (entryUrl.isEmpty()) {
return false; return false;
} }
bool isWildcardUrl = false;
auto tempUrl = entryUrl;
// Allows matching with exact URL and wildcards
if (allowWildcards) {
// Exact match where URL is wrapped inside " characters
if (entryUrl.startsWith("\"") && entryUrl.endsWith("\"")) {
return QStringView{entryUrl}.mid(1, entryUrl.length() - 2) == siteUrl;
}
// Replace wildcards
isWildcardUrl = entryUrl.contains("*");
if (isWildcardUrl) {
tempUrl = tempUrl.replace("*", UrlTools::URL_WILDCARD);
}
}
QUrl entryQUrl; QUrl entryQUrl;
if (entryUrl.contains("://")) { if (entryUrl.contains("://")) {
entryQUrl = entryUrl; entryQUrl = tempUrl;
} else { } else {
entryQUrl = QUrl::fromUserInput(entryUrl); entryQUrl = QUrl::fromUserInput(tempUrl);
if (browserSettings()->matchUrlScheme()) { if (browserSettings()->matchUrlScheme()) {
entryQUrl.setScheme("https"); entryQUrl.setScheme("https");
@ -1515,6 +1540,11 @@ bool BrowserService::handleURL(const QString& entryUrl,
return false; return false;
} }
// Use wildcard matching instead
if (isWildcardUrl) {
return handleURLWithWildcards(entryQUrl, siteUrl);
}
// Match the base domain // Match the base domain
if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) { if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) {
return false; return false;
@ -1528,6 +1558,46 @@ bool BrowserService::handleURL(const QString& entryUrl,
return false; return false;
} }
bool BrowserService::handleURLWithWildcards(const QUrl& entryQUrl, const QString& siteUrl)
{
auto matchWithRegex = [&](QString firstPart, const QString& secondPart, bool hostnameUsed = false) {
if (firstPart == secondPart) {
return true;
}
// If there's no wildcard with hostname, just compare directly
if (hostnameUsed && !firstPart.contains(UrlTools::URL_WILDCARD) && firstPart != secondPart) {
return false;
}
// Escape illegal characters
auto re = firstPart.replace(QRegularExpression(R"(([!\^\$\+\-\(\)@<>]))"), "\\\\1");
if (hostnameUsed) {
// Replace all host parts with wildcards
re = re.replace(QString("%1.").arg(UrlTools::URL_WILDCARD), "(.*?)");
}
// Append a + to the end of regex to match all paths after the last asterisk
if (re.endsWith(UrlTools::URL_WILDCARD)) {
re.append("+");
}
// Replace any remaining wildcards for paths
re = re.replace(UrlTools::URL_WILDCARD, "(.*?)");
return QRegularExpression(re).match(secondPart).hasMatch();
};
// Match hostname and path
QUrl siteQUrl = siteUrl;
if (!matchWithRegex(entryQUrl.host(), siteQUrl.host(), true)
|| !matchWithRegex(entryQUrl.path(), siteQUrl.path())) {
return false;
}
return true;
}
QSharedPointer<Database> BrowserService::getDatabase(const QUuid& rootGroupUuid) QSharedPointer<Database> BrowserService::getDatabase(const QUuid& rootGroupUuid)
{ {
if (!rootGroupUuid.isNull()) { if (!rootGroupUuid.isNull()) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> * Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand * Copyright (C) 2013 Francois Ferrand
* *
@ -132,6 +132,7 @@ public:
static const QString OPTION_ONLY_HTTP_AUTH; static const QString OPTION_ONLY_HTTP_AUTH;
static const QString OPTION_NOT_HTTP_AUTH; static const QString OPTION_NOT_HTTP_AUTH;
static const QString OPTION_OMIT_WWW; static const QString OPTION_OMIT_WWW;
static const QString ADDITIONAL_URL;
static const QString OPTION_RESTRICT_KEY; static const QString OPTION_RESTRICT_KEY;
signals: signals:
@ -199,7 +200,9 @@ private:
bool handleURL(const QString& entryUrl, bool handleURL(const QString& entryUrl,
const QString& siteUrl, const QString& siteUrl,
const QString& formUrl, const QString& formUrl,
const bool omitWwwSubdomain = false); const bool omitWwwSubdomain = false,
const bool allowWildcards = false);
bool handleURLWithWildcards(const QUrl& entryQUrl, const QString& siteUrl);
QString getDatabaseRootUuid(); QString getDatabaseRootUuid();
QString getDatabaseRecycleBinUuid(); QString getDatabaseRecycleBinUuid();
void hideWindow() const; void hideWindow() const;

View File

@ -381,16 +381,32 @@ QString Entry::url() const
return m_attributes->value(EntryAttributes::URLKey); return m_attributes->value(EntryAttributes::URLKey);
} }
QString Entry::resolveUrl() const
{
const auto entryUrl = url();
if (entryUrl.isEmpty()) {
return {};
}
return EntryAttributes::matchReference(entryUrl).hasMatch() ? resolveMultiplePlaceholders(entryUrl) : entryUrl;
}
QStringList Entry::getAllUrls() const QStringList Entry::getAllUrls() const
{ {
QStringList urlList; QStringList urlList;
auto entryUrl = url();
const auto entryUrl = resolveUrl();
if (!entryUrl.isEmpty()) { if (!entryUrl.isEmpty()) {
urlList << (EntryAttributes::matchReference(entryUrl).hasMatch() ? resolveMultiplePlaceholders(entryUrl) urlList << entryUrl;
: entryUrl);
} }
return urlList << getAdditionalUrls();
}
QStringList Entry::getAdditionalUrls() const
{
QStringList urlList;
for (const auto& key : m_attributes->keys()) { for (const auto& key : m_attributes->keys()) {
if (key.startsWith(EntryAttributes::AdditionalUrlAttribute) if (key.startsWith(EntryAttributes::AdditionalUrlAttribute)
|| key == QString("%1_RELYING_PARTY").arg(EntryAttributes::PasskeyAttribute)) { || key == QString("%1_RELYING_PARTY").arg(EntryAttributes::PasskeyAttribute)) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de> * Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -100,7 +100,9 @@ public:
const AutoTypeAssociations* autoTypeAssociations() const; const AutoTypeAssociations* autoTypeAssociations() const;
QString title() const; QString title() const;
QString url() const; QString url() const;
QString resolveUrl() const;
QStringList getAllUrls() const; QStringList getAllUrls() const;
QStringList getAdditionalUrls() const;
QString webUrl() const; QString webUrl() const;
QString displayUrl() const; QString displayUrl() const;
QString username() const; QString username() const;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,6 +24,8 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QUrl> #include <QUrl>
const QString UrlTools::URL_WILDCARD = "1kpxcwc1";
Q_GLOBAL_STATIC(UrlTools, s_urlTools) Q_GLOBAL_STATIC(UrlTools, s_urlTools)
UrlTools* UrlTools::instance() UrlTools* UrlTools::instance()
@ -137,8 +139,9 @@ bool UrlTools::isUrlIdentical(const QString& first, const QString& second) const
return false; return false;
} }
const auto firstUrl = trimUrl(first); // Replace URL wildcards for comparison if found
const auto secondUrl = trimUrl(second); const auto firstUrl = trimUrl(QString(first).replace("*", UrlTools::URL_WILDCARD));
const auto secondUrl = trimUrl(QString(second).replace("*", UrlTools::URL_WILDCARD));
if (firstUrl == secondUrl) { if (firstUrl == secondUrl) {
return true; return true;
} }
@ -146,27 +149,61 @@ bool UrlTools::isUrlIdentical(const QString& first, const QString& second) const
return QUrl(firstUrl).matches(QUrl(secondUrl), QUrl::StripTrailingSlash); return QUrl(firstUrl).matches(QUrl(secondUrl), QUrl::StripTrailingSlash);
} }
bool UrlTools::isUrlValid(const QString& urlField) const bool UrlTools::isUrlValid(const QString& urlField, bool looseComparison) const
{ {
if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive) if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive)
|| urlField.startsWith("kdbx://", Qt::CaseInsensitive) || urlField.startsWith("{REF:A", Qt::CaseInsensitive)) { || urlField.startsWith("kdbx://", Qt::CaseInsensitive) || urlField.startsWith("{REF:A", Qt::CaseInsensitive)) {
return true; return true;
} }
QUrl url; auto url = urlField;
if (urlField.contains("://")) {
url = urlField; // Loose comparison that allows wildcards and exact URL inside " characters
} else { if (looseComparison) {
url = QUrl::fromUserInput(urlField); // Exact URL
if (url.startsWith("\"") && url.endsWith("\"")) {
// Do not allow exact URL with wildcards, or empty exact URL
if (url.contains("*") || url.length() == 2) {
return false;
}
// Get the URL inside ""
url.remove(0, 1);
url.remove(url.length() - 1, 1);
} else {
// Do not allow URL with just wildcards, or double wildcards, or no separator (.)
if (url.length() == url.count("*") || url.contains("**") || url.contains("*.*") || !url.contains(".")) {
return false;
}
url.replace("*", UrlTools::URL_WILDCARD);
}
} }
if (url.scheme() != "file" && url.host().isEmpty()) { QUrl qUrl;
if (urlField.contains("://")) {
qUrl = url;
} else {
qUrl = QUrl::fromUserInput(url);
}
if (qUrl.scheme() != "file" && qUrl.host().isEmpty()) {
return false; return false;
} }
#if defined(WITH_XC_NETWORKING) || defined(WITH_XC_BROWSER)
// Prevent TLD wildcards
if (looseComparison && url.contains(UrlTools::URL_WILDCARD)) {
const auto tld = getTopLevelDomainFromUrl(url);
if (qUrl.host() == QString("%1.%2").arg(UrlTools::URL_WILDCARD, tld)) {
return false;
}
}
#endif
// Check for illegal characters. Adds also the wildcard * to the list // Check for illegal characters. Adds also the wildcard * to the list
QRegularExpression re("[<>\\^`{|}\\*]"); QRegularExpression re("[<>\\^`{|}\\*]");
auto match = re.match(urlField); auto match = re.match(url);
if (match.hasMatch()) { if (match.hasMatch()) {
return false; return false;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -41,9 +41,11 @@ public:
bool isIpAddress(const QString& host) const; bool isIpAddress(const QString& host) const;
#endif #endif
bool isUrlIdentical(const QString& first, const QString& second) const; bool isUrlIdentical(const QString& first, const QString& second) const;
bool isUrlValid(const QString& urlField) const; bool isUrlValid(const QString& urlField, bool looseComparison = false) const;
bool domainHasIllegalCharacters(const QString& domain) const; bool domainHasIllegalCharacters(const QString& domain) const;
static const QString URL_WILDCARD;
private: private:
QUrl convertVariantToUrl(const QVariant& var) const; QUrl convertVariantToUrl(const QVariant& var) const;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de> * Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -67,7 +67,7 @@ QVariant EntryURLModel::data(const QModelIndex& index, int role) const
} }
const auto value = m_entryAttributes->value(key); const auto value = m_entryAttributes->value(key);
const auto urlValid = urlTools()->isUrlValid(value); const auto urlValid = urlTools()->isUrlValid(value, true);
// Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison. // Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison.
auto customAttributeKeys = m_entryAttributes->customKeys().filter(EntryAttributes::AdditionalUrlAttribute); auto customAttributeKeys = m_entryAttributes->customKeys().filter(EntryAttributes::AdditionalUrlAttribute);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -222,7 +222,7 @@ void TestBrowser::testSearchEntries()
QCOMPARE(result[4]->url(), QString("http://github.com")); QCOMPARE(result[4]->url(), QString("http://github.com"));
QCOMPARE(result[5]->url(), QString("http://github.com/login")); QCOMPARE(result[5]->url(), QString("http://github.com/login"));
// With matching there should be only 3 results + 4 without a scheme // With matching there should be only 4 results + 4 without a scheme
browserSettings()->setMatchUrlScheme(true); browserSettings()->setMatchUrlScheme(true);
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 7); QCOMPARE(result.length(), 7);
@ -396,6 +396,121 @@ void TestBrowser::testSearchEntriesWithAdditionalURLs()
QCOMPARE(additionalResult[0]->url(), QString("https://github.com/")); QCOMPARE(additionalResult[0]->url(), QString("https://github.com/"));
} }
void TestBrowser::testSearchEntriesWithWildcardURLs()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
QStringList urls = {
"https://github.com/login_page/*",
"https://github.com/*/second",
"https://github.com/*",
"http://github.com/*",
"github.com/*", // Defaults to https
"https://*.github.com/*",
"https://subdomain.*.github.com/*/second",
"https://*.sub.github.com/*",
"https://********", // Invalid wildcard URL
"https://subdomain.yes.github.com/*",
"https://example.com:8448/*",
"https://example.com/*/*",
"https://example.com/$/*",
"https://127.128.129.*:8448/",
"https://127.128.*/",
"https://127.160.*.2/login",
"http://[2001:db8:85a3:8d3:1319:8a2e:370:*]/",
"https://[2001:db8:85a3:8d3:*]:443/",
"fe80::1ff:fe23:4567:890a",
"2001-db8-85a3-8d3-1319-8a2e-370-7348.ipv6-literal.net",
"\"https://thisisatest.com/login.php\"" // Exact URL
};
createEntries(urls, root, true);
browserSettings()->setMatchUrlScheme(false);
// Return first Additional URL
auto firstUrl = [&](Entry* entry) { return entry->attributes()->value(EntryAttributes::AdditionalUrlAttribute); };
auto result = m_browserService->searchEntries(
db, "https://github.com/login_page/second", "https://github.com/login_page/second");
QCOMPARE(result.length(), 6);
QCOMPARE(firstUrl(result[0]), QString("https://github.com/login_page/*"));
QCOMPARE(firstUrl(result[1]), QString("https://github.com/*/second"));
QCOMPARE(firstUrl(result[2]), QString("https://github.com/*"));
QCOMPARE(firstUrl(result[3]), QString("http://github.com/*"));
QCOMPARE(firstUrl(result[4]), QString("github.com/*"));
QCOMPARE(firstUrl(result[5]), QString("https://*.github.com/*"));
result = m_browserService->searchEntries(
db, "https://subdomain.sub.github.com/login_page/second", "https://subdomain.sub.github.com/login_page/second");
QCOMPARE(result.length(), 3);
QCOMPARE(firstUrl(result[0]), QString("https://*.github.com/*"));
QCOMPARE(firstUrl(result[1]), QString("https://subdomain.*.github.com/*/second"));
QCOMPARE(firstUrl(result[2]), QString("https://*.sub.github.com/*"));
result = m_browserService->searchEntries(
db, "https://subdomain.sub.github.com/other_page", "https://subdomain.sub.github.com/other_page");
QCOMPARE(result.length(), 2);
QCOMPARE(firstUrl(result[0]), QString("https://*.github.com/*"));
QCOMPARE(firstUrl(result[1]), QString("https://*.sub.github.com/*"));
result = m_browserService->searchEntries(
db, "https://subdomain.yes.github.com/other_page/second", "https://subdomain.yes.github.com/other_page/second");
QCOMPARE(result.length(), 3);
QCOMPARE(firstUrl(result[0]), QString("https://*.github.com/*"));
QCOMPARE(firstUrl(result[1]), QString("https://subdomain.*.github.com/*/second"));
QCOMPARE(firstUrl(result[2]), QString("https://subdomain.yes.github.com/*"));
result = m_browserService->searchEntries(
db, "https://example.com:8448/login/page", "https://example.com:8448/login/page");
QCOMPARE(result.length(), 2);
QCOMPARE(firstUrl(result[0]), QString("https://example.com:8448/*"));
QCOMPARE(firstUrl(result[1]), QString("https://example.com/*/*"));
result = m_browserService->searchEntries(
db, "https://example.com:8449/login/page", "https://example.com:8449/login/page");
QCOMPARE(result.length(), 1);
QCOMPARE(firstUrl(result[0]), QString("https://example.com/*/*"));
result =
m_browserService->searchEntries(db, "https://example.com/$/login_page", "https://example.com/$/login_page");
QCOMPARE(result.length(), 2);
QCOMPARE(firstUrl(result[0]), QString("https://example.com/*/*"));
QCOMPARE(firstUrl(result[1]), QString("https://example.com/$/*"));
result = m_browserService->searchEntries(db, "https://127.128.129.130:8448/", "https://127.128.129.130:8448/");
QCOMPARE(result.length(), 2);
result = m_browserService->searchEntries(db, "https://127.128.129.130/", "https://127.128.129.130/");
QCOMPARE(result.length(), 1);
QCOMPARE(firstUrl(result[0]), QString("https://127.128.*/"));
result = m_browserService->searchEntries(db, "https://127.1.129.130/", "https://127.1.129.130/");
QCOMPARE(result.length(), 0);
result = m_browserService->searchEntries(db, "https://127.160.8.2/login", "https://127.160.8.2/login");
QCOMPARE(result.length(), 1);
QCOMPARE(firstUrl(result[0]), QString("https://127.160.*.2/login"));
// Exact URL
result =
m_browserService->searchEntries(db, "https://thisisatest.com/login.php", "https://thisisatest.com/login.php");
QCOMPARE(result.length(), 1);
QCOMPARE(firstUrl(result[0]), QString("\"https://thisisatest.com/login.php\""));
// With scheme matching enabled
browserSettings()->setMatchUrlScheme(true);
result = m_browserService->searchEntries(
db, "https://github.com/login_page/second", "https://github.com/login_page/second");
QCOMPARE(result.length(), 5);
QCOMPARE(firstUrl(result[0]), QString("https://github.com/login_page/*"));
QCOMPARE(firstUrl(result[1]), QString("https://github.com/*/second"));
QCOMPARE(firstUrl(result[2]), QString("https://github.com/*"));
QCOMPARE(firstUrl(result[3]), QString("github.com/*")); // Defaults to https
QCOMPARE(firstUrl(result[4]), QString("https://*.github.com/*"));
}
void TestBrowser::testInvalidEntries() void TestBrowser::testInvalidEntries()
{ {
auto db = QSharedPointer<Database>::create(); auto db = QSharedPointer<Database>::create();
@ -516,14 +631,18 @@ void TestBrowser::testSubdomainsAndPaths()
QCOMPARE(result.length(), 1); QCOMPARE(result.length(), 1);
} }
QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root) const QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root, bool additionalUrl) const
{ {
QList<Entry*> entries; QList<Entry*> entries;
for (int i = 0; i < urls.length(); ++i) { for (int i = 0; i < urls.length(); ++i) {
auto entry = new Entry(); auto entry = new Entry();
entry->setGroup(root); entry->setGroup(root);
entry->beginUpdate(); entry->beginUpdate();
entry->setUrl(urls[i]); if (additionalUrl) {
entry->attributes()->set(EntryAttributes::AdditionalUrlAttribute, urls[i]);
} else {
entry->setUrl(urls[i]);
}
entry->setUsername(QString("User %1").arg(i)); entry->setUsername(QString("User %1").arg(i));
entry->setUuid(QUuid::createUuid()); entry->setUuid(QUuid::createUuid());
entry->setTitle(QString("Name_%1").arg(entry->uuidToHex())); entry->setTitle(QString("Name_%1").arg(entry->uuidToHex()));

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -45,6 +45,7 @@ private slots:
void testSearchEntriesByReference(); void testSearchEntriesByReference();
void testSearchEntriesWithPort(); void testSearchEntriesWithPort();
void testSearchEntriesWithAdditionalURLs(); void testSearchEntriesWithAdditionalURLs();
void testSearchEntriesWithWildcardURLs();
void testInvalidEntries(); void testInvalidEntries();
void testSubdomainsAndPaths(); void testSubdomainsAndPaths();
void testBestMatchingCredentials(); void testBestMatchingCredentials();
@ -52,7 +53,7 @@ private slots:
void testRestrictBrowserKey(); void testRestrictBrowserKey();
private: private:
QList<Entry*> createEntries(QStringList& urls, Group* root) const; QList<Entry*> createEntries(QStringList& urls, Group* root, bool additionalUrl = false) const;
void compareEntriesByPath(QSharedPointer<Database> db, QList<Entry*> entries, QString path); void compareEntriesByPath(QSharedPointer<Database> db, QList<Entry*> entries, QString path);
QScopedPointer<BrowserAction> m_browserAction; QScopedPointer<BrowserAction> m_browserAction;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -136,6 +136,36 @@ void TestUrlTools::testIsUrlValid()
} }
} }
void TestUrlTools::testIsUrlValidWithLooseComparison()
{
QHash<QString, bool> urls;
urls[""] = true;
urls["\"https://github.com/login\""] = true;
urls["https://*.github.com/"] = true;
urls["*.github.com"] = true;
urls["https://*.com"] = false;
urls["https://*.computer.com"] = true; // TLD in domain (com) should not affect
urls["\"\""] = false;
urls["\"*.example.com\""] = false;
urls["http://*"] = false;
urls["*"] = false;
urls["****"] = false;
urls["*.co.jp"] = false;
urls["*.com"] = false;
urls["*.computer.com"] = true;
urls["*.computer.com/*com"] = true; // TLD in path should not affect this
urls["*com"] = false;
urls["*.com/"] = false;
urls["*.com/*"] = false;
urls["**.com/**"] = false;
QHashIterator<QString, bool> i(urls);
while (i.hasNext()) {
i.next();
QCOMPARE(urlTools()->isUrlValid(i.key(), true), i.value());
}
}
void TestUrlTools::testDomainHasIllegalCharacters() void TestUrlTools::testDomainHasIllegalCharacters()
{ {
QVERIFY(!urlTools()->domainHasIllegalCharacters("example.com")); QVERIFY(!urlTools()->domainHasIllegalCharacters("example.com"));

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -34,6 +34,7 @@ private slots:
void testIsIpAddress(); void testIsIpAddress();
void testIsUrlIdentical(); void testIsUrlIdentical();
void testIsUrlValid(); void testIsUrlValid();
void testIsUrlValidWithLooseComparison();
void testDomainHasIllegalCharacters(); void testDomainHasIllegalCharacters();
private: private: