Skip to content

Commit

Permalink
Add support for URL wildcards with Additional URL feature
Browse files Browse the repository at this point in the history
  • Loading branch information
varjolintu committed Feb 3, 2025
1 parent 15ac8ac commit 590cd3c
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 34 deletions.
84 changes: 77 additions & 7 deletions src/browser/BrowserService.cpp
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
*
Expand Down Expand Up @@ -51,6 +51,7 @@
#include <QLocalSocket>
#include <QLocale>
#include <QProgressDialog>
#include <QStringView>
#include <QUrl>

const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand All @@ -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()) {
Expand Down
7 changes: 5 additions & 2 deletions src/browser/BrowserService.h
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
*
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 19 additions & 3 deletions src/core/Entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -381,16 +381,32 @@ QString Entry::url() const
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 urlList;
auto entryUrl = url();

const auto entryUrl = resolveUrl();
if (!entryUrl.isEmpty()) {
urlList << (EntryAttributes::matchReference(entryUrl).hasMatch() ? resolveMultiplePlaceholders(entryUrl)
: entryUrl);
urlList << entryUrl;
}

return urlList << getAdditionalUrls();
}

QStringList Entry::getAdditionalUrls() const
{
QStringList urlList;

for (const auto& key : m_attributes->keys()) {
if (key.startsWith(EntryAttributes::AdditionalUrlAttribute)
|| key == QString("%1_RELYING_PARTY").arg(EntryAttributes::PasskeyAttribute)) {
Expand Down
4 changes: 3 additions & 1 deletion src/core/Entry.h
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
Expand Down Expand Up @@ -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;
Expand Down
53 changes: 44 additions & 9 deletions src/gui/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) 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
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,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;
}
Expand Down
6 changes: 4 additions & 2 deletions src/gui/UrlTools.h
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
Expand Down Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions src/gui/entry/EntryURLModel.cpp
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
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 590cd3c

Please sign in to comment.