-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for URL wildcards with Additional URL feature
- Loading branch information
varjolintu
committed
Feb 3, 2025
1 parent
15ac8ac
commit 590cd3c
Showing
11 changed files
with
313 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (C) 2024 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2025 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2017 Sami Vänttinen <[email protected]> | ||
* Copyright (C) 2013 Francois Ferrand | ||
* | ||
|
@@ -51,6 +51,7 @@ | |
#include <QLocalSocket> | ||
#include <QLocale> | ||
#include <QProgressDialog> | ||
#include <QStringView> | ||
#include <QUrl> | ||
|
||
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()); | ||
} | ||
|
||
const auto allEntryUrls = entry->getAllUrls(); | ||
for (const auto& entryUrl : allEntryUrls) { | ||
if (handleURL(entryUrl, url, submitUrl, omitWwwSubdomain)) { | ||
// Handle the entry URL | ||
if (handleURL(entry->resolveUrl(), 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; | ||
} | ||
} | ||
|
@@ -1465,17 +1472,35 @@ QJsonObject BrowserService::getPasskeyError(int errorCode) const | |
bool BrowserService::handleURL(const QString& entryUrl, | ||
const QString& siteUrl, | ||
const QString& formUrl, | ||
const bool omitWwwSubdomain) | ||
const bool omitWwwSubdomain, | ||
const bool allowWildcards) | ||
{ | ||
if (entryUrl.isEmpty()) { | ||
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; | ||
if (entryUrl.contains("://")) { | ||
entryQUrl = entryUrl; | ||
entryQUrl = tempUrl; | ||
} else { | ||
entryQUrl = QUrl::fromUserInput(entryUrl); | ||
entryQUrl = QUrl::fromUserInput(tempUrl); | ||
|
||
if (browserSettings()->matchUrlScheme()) { | ||
entryQUrl.setScheme("https"); | ||
|
@@ -1515,6 +1540,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; | ||
|
@@ -1528,6 +1558,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()) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (C) 2024 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2025 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2017 Sami Vänttinen <[email protected]> | ||
* Copyright (C) 2013 Francois Ferrand | ||
* | ||
|
@@ -132,6 +132,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: | ||
|
@@ -199,7 +200,9 @@ private slots: | |
bool handleURL(const QString& entryUrl, | ||
const QString& siteUrl, | ||
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 getDatabaseRecycleBinUuid(); | ||
void hideWindow() const; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (C) 2024 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2025 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2010 Felix Geyer <[email protected]> | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
|
@@ -100,7 +100,9 @@ class Entry : public ModifiableObject | |
const AutoTypeAssociations* autoTypeAssociations() const; | ||
QString title() const; | ||
QString url() const; | ||
QString resolveUrl() const; | ||
QStringList getAllUrls() const; | ||
QStringList getAdditionalUrls() const; | ||
QString webUrl() const; | ||
QString displayUrl() const; | ||
QString username() const; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (C) 2023 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2025 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 | ||
|
@@ -24,6 +24,8 @@ | |
#include <QRegularExpression> | ||
#include <QUrl> | ||
|
||
const QString UrlTools::URL_WILDCARD = "1kpxcwc1"; | ||
|
||
Q_GLOBAL_STATIC(UrlTools, s_urlTools) | ||
|
||
UrlTools* UrlTools::instance() | ||
|
@@ -137,36 +139,69 @@ 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; | ||
} | ||
|
||
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) | ||
|| urlField.startsWith("kdbx://", Qt::CaseInsensitive) || urlField.startsWith("{REF:A", Qt::CaseInsensitive)) { | ||
return true; | ||
} | ||
|
||
QUrl url; | ||
auto url = urlField; | ||
|
||
// Loose comparison that allows wildcards and exact URL inside " characters | ||
if (looseComparison) { | ||
// 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); | ||
} | ||
} | ||
|
||
QUrl qUrl; | ||
if (urlField.contains("://")) { | ||
url = urlField; | ||
qUrl = url; | ||
} else { | ||
url = QUrl::fromUserInput(urlField); | ||
qUrl = QUrl::fromUserInput(url); | ||
} | ||
|
||
if (url.scheme() != "file" && url.host().isEmpty()) { | ||
if (qUrl.scheme() != "file" && qUrl.host().isEmpty()) { | ||
return false; | ||
} | ||
|
||
// Prevent TLD wildcards | ||
if (looseComparison) { | ||
const auto tld = getTopLevelDomainFromUrl(url); | ||
if (qUrl.host() == QString("%1.%2").arg(UrlTools::URL_WILDCARD, tld)) { | ||
return false; | ||
} | ||
} | ||
|
||
// Check for illegal characters. Adds also the wildcard * to the list | ||
QRegularExpression re("[<>\\^`{|}\\*]"); | ||
auto match = re.match(urlField); | ||
auto match = re.match(url); | ||
if (match.hasMatch()) { | ||
return false; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (C) 2024 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2025 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 | ||
|
@@ -41,9 +41,11 @@ class UrlTools : public QObject | |
bool isIpAddress(const QString& host) const; | ||
#endif | ||
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; | ||
|
||
static const QString URL_WILDCARD; | ||
|
||
private: | ||
QUrl convertVariantToUrl(const QVariant& var) const; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (C) 2023 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2025 KeePassXC Team <[email protected]> | ||
* Copyright (C) 2012 Felix Geyer <[email protected]> | ||
* | ||
* 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 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. | ||
auto customAttributeKeys = m_entryAttributes->customKeys().filter(EntryAttributes::AdditionalUrlAttribute); | ||
|
Oops, something went wrong.