Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for URL wildcards and exact URL #9835

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
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
64 changes: 61 additions & 3 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1439,11 +1439,24 @@ bool BrowserService::handleURL(const QString& entryUrl,
return false;
}

// Exact match where URL is wrapped inside " characters
if (entryUrl.startsWith("\"") && entryUrl.endsWith("\"") && entryUrl.midRef(1, entryUrl.length() - 2) == siteUrl) {
return true;
}

const auto isWildcardUrl = entryUrl.contains("*");

// Replace wildcards
auto tempUrl = entryUrl;
if (isWildcardUrl) {
tempUrl = tempUrl.replace("*", UrlTools::URL_WILDCARD);
}

QUrl entryQUrl;
if (entryUrl.contains("://")) {
entryQUrl = entryUrl;
entryQUrl = tempUrl;
} else {
entryQUrl = QUrl::fromUserInput(entryUrl);
entryQUrl = QUrl::fromUserInput(tempUrl);

if (browserSettings()->matchUrlScheme()) {
entryQUrl.setScheme("https");
Expand All @@ -1467,7 +1480,7 @@ bool BrowserService::handleURL(const QString& entryUrl,

// Match port, if used
QUrl siteQUrl(siteUrl);
if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) {
if ((entryQUrl.port() > 0) && entryQUrl.port() != siteQUrl.port()) {
return false;
}

Expand All @@ -1483,6 +1496,11 @@ bool BrowserService::handleURL(const QString& entryUrl,
return false;
}

// Use wildcard matching instead
if (isWildcardUrl) {
return handleURLWithWildcards(entryQUrl, siteUrl);
}

// Match the base domain
if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) {
return false;
Expand All @@ -1496,6 +1514,46 @@ bool BrowserService::handleURL(const QString& entryUrl,
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)
{
if (!rootGroupUuid.isNull()) {
Expand Down
2 changes: 2 additions & 0 deletions src/browser/BrowserService.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class BrowserService : public QObject
static const QString OPTION_ONLY_HTTP_AUTH;
static const QString OPTION_NOT_HTTP_AUTH;
static const QString OPTION_OMIT_WWW;
static const QString ADDITIONAL_URL;
static const QString OPTION_RESTRICT_KEY;

signals:
Expand Down Expand Up @@ -197,6 +198,7 @@ private slots:
const QString& siteUrl,
const QString& formUrl,
const bool omitWwwSubdomain = false);
bool handleURLWithWildcards(const QUrl& entryQUrl, const QString& siteUrl);
QString getDatabaseRootUuid();
QString getDatabaseRecycleBinUuid();
void hideWindow() const;
Expand Down
9 changes: 6 additions & 3 deletions src/core/UrlTools.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <[email protected]>
* Copyright (C) 2024 KeePassXC Team <[email protected]>
*
* 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
Expand All @@ -24,6 +24,8 @@
#include <QRegularExpression>
#include <QUrl>

const QString UrlTools::URL_WILDCARD = "1kpxcwc1";

Q_GLOBAL_STATIC(UrlTools, s_urlTools)

UrlTools* UrlTools::instance()
Expand Down Expand Up @@ -137,8 +139,9 @@ bool UrlTools::isUrlIdentical(const QString& first, const QString& second) const
return false;
}

const auto firstUrl = trimUrl(first);
const auto secondUrl = trimUrl(second);
// Replace URL wildcards for comparison if found
const auto firstUrl = trimUrl(QString(first).replace("*", UrlTools::URL_WILDCARD));
const auto secondUrl = trimUrl(QString(second).replace("*", UrlTools::URL_WILDCARD));
if (firstUrl == secondUrl) {
return true;
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/UrlTools.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class UrlTools : public QObject
bool isUrlValid(const QString& urlField) const;
bool domainHasIllegalCharacters(const QString& domain) const;

static const QString URL_WILDCARD;

private:
QUrl convertVariantToUrl(const QVariant& var) const;

Expand Down
132 changes: 119 additions & 13 deletions tests/TestBrowser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,35 +201,38 @@ void TestBrowser::testSearchEntries()
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();

QStringList urls = {"https://github.com/login_page",
"https://github.com/login",
"https://github.com/",
"github.com/login",
"http://github.com",
"http://github.com/login",
"github.com",
"github.com/login",
"https://github", // Invalid URL
"github.com"};
QStringList urls = {
"https://github.com/login_page",
"https://github.com/login",
"https://github.com/",
"github.com/login",
"http://github.com",
"http://github.com/login",
"github.com",
"github.com/login",
"https://github", // Invalid URL
"github.com",
"\"https://github.com\"" // Exact URL
};

createEntries(urls, root);

browserSettings()->setMatchUrlScheme(false);
auto result =
m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); // db, url, submitUrl

QCOMPARE(result.length(), 9);
QCOMPARE(result.length(), 10);
QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
QCOMPARE(result[1]->url(), QString("https://github.com/login"));
QCOMPARE(result[2]->url(), QString("https://github.com/"));
QCOMPARE(result[3]->url(), QString("github.com/login"));
QCOMPARE(result[4]->url(), QString("http://github.com"));
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);
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 7);
QCOMPARE(result.length(), 8);
QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
QCOMPARE(result[1]->url(), QString("https://github.com/login"));
QCOMPARE(result[2]->url(), QString("https://github.com/"));
Expand Down Expand Up @@ -400,6 +403,109 @@ void TestBrowser::testSearchEntriesWithAdditionalURLs()
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"};

createEntries(urls, root);
browserSettings()->setMatchUrlScheme(false);

auto result = m_browserService->searchEntries(
db, "https://github.com/login_page/second", "https://github.com/login_page/second");
QCOMPARE(result.length(), 6);
QCOMPARE(result[0]->url(), QString("https://github.com/login_page/*"));
QCOMPARE(result[1]->url(), QString("https://github.com/*/second"));
QCOMPARE(result[2]->url(), QString("https://github.com/*"));
QCOMPARE(result[3]->url(), QString("http://github.com/*"));
QCOMPARE(result[4]->url(), QString("github.com/*"));
QCOMPARE(result[5]->url(), 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(result[0]->url(), QString("https://*.github.com/*"));
QCOMPARE(result[1]->url(), QString("https://subdomain.*.github.com/*/second"));
QCOMPARE(result[2]->url(), 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(result[0]->url(), QString("https://*.github.com/*"));
QCOMPARE(result[1]->url(), 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(result[0]->url(), QString("https://*.github.com/*"));
QCOMPARE(result[1]->url(), QString("https://subdomain.*.github.com/*/second"));
QCOMPARE(result[2]->url(), 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(result[0]->url(), QString("https://example.com:8448/*"));
QCOMPARE(result[1]->url(), 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(result[0]->url(), QString("https://example.com/*/*"));

result =
m_browserService->searchEntries(db, "https://example.com/$/login_page", "https://example.com/$/login_page");
QCOMPARE(result.length(), 2);
QCOMPARE(result[0]->url(), QString("https://example.com/*/*"));
QCOMPARE(result[1]->url(), 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(result[0]->url(), 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(result[0]->url(), QString("https://127.160.*.2/login"));

// 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(result[0]->url(), QString("https://github.com/login_page/*"));
QCOMPARE(result[1]->url(), QString("https://github.com/*/second"));
QCOMPARE(result[2]->url(), QString("https://github.com/*"));
QCOMPARE(result[3]->url(), QString("github.com/*")); // Defaults to https
QCOMPARE(result[4]->url(), QString("https://*.github.com/*"));
}

void TestBrowser::testInvalidEntries()
{
auto db = QSharedPointer<Database>::create();
Expand Down
1 change: 1 addition & 0 deletions tests/TestBrowser.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ private slots:
void testSearchEntriesByReference();
void testSearchEntriesWithPort();
void testSearchEntriesWithAdditionalURLs();
void testSearchEntriesWithWildcardURLs();
void testInvalidEntries();
void testSubdomainsAndPaths();
void testBestMatchingCredentials();
Expand Down
Loading