1
0
mirror of https://github.com/qt/qtmqtt.git synced 2026-10-25 16:16:26 +00:00
qtmqtt/src/mqtt/qmqtttopicfilter.cpp
Ivan Solovev 7674ec983f Mark qmqtttopicfilter.cpp as security-critical
The QMqttTopicFilter::isValid() method parses the user-provided
string and validates it according to the MQTT protocol.

Task-number: QTBUG-135764
Pick-to: 6.10 6.9 6.8
Change-Id: I841bf83bbf29a2cdf4cf3faec93615a33f597241
Reviewed-by: Matthias Rauter <matthias.rauter@qt.io>
2025-07-29 14:06:17 +02:00

332 lines
9.2 KiB
C++

// Copyright (C) 2017 Lorenz Haas
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qmqtttopicfilter.h"
#include <QtCore/QDebug>
#include <QtCore/QList>
QT_BEGIN_NAMESPACE
/*!
\class QMqttTopicFilter
\inmodule QtMqtt
\reentrant
\ingroup shared
\brief The QMqttTopicFilter class represents a MQTT topic filter.
QMqttTopicFilter is a thin wrapper around a QString providing an expressive
data type for MQTT topic filters. Beside the benefits of having a strong
type preventing unintended misuse, QMqttTopicFilter provides convenient
functions related to topic filters like isValid() or match().
For example, the following code would fail to compile and prevent a possible
unintended and meaningless matching of two filters, especially if the
variable names were less expressive:
\code
QMqttTopicFilter globalFilter{"foo/#"};
QMqttTopicFilter specificFilter{"foo/bar"};
if (globalFilter.match(specificFilter)) {
//...
}
\endcode
The usability, however, is not affected since the following snippet compiles
and runs as expected:
\code
QMqttTopicFilter globalFilter{"foo/#"};
if (globalFilter.match("foo/bar")) {
//...
}
\endcode
\sa QMqttTopicName
*/
/*!
\fn void QMqttTopicFilter::swap(QMqttTopicFilter &other)
Swaps the MQTT topic filter \a other with this MQTT topic filter. This
operation is very fast and never fails.
*/
/*!
\enum QMqttTopicFilter::MatchOption
This enum value holds the matching options for the topic filter.
\value NoMatchOption
No match options are set.
\value WildcardsDontMatchDollarTopicMatchOption
A wildcard at the filter's beginning does not match a topic name that
starts with the dollar sign ($).
*/
class QMqttTopicFilterPrivate : public QSharedData
{
public:
QString filter;
};
/*!
Creates a new MQTT topic filter with the specified \a filter.
*/
QMqttTopicFilter::QMqttTopicFilter(const QString &filter) : d(new QMqttTopicFilterPrivate)
{
d->filter = filter;
}
/*!
Creates a new MQTT topic filter with the specified \a filter.
*/
QMqttTopicFilter::QMqttTopicFilter(const QLatin1String &filter) : d(new QMqttTopicFilterPrivate)
{
d->filter = filter;
}
/*!
Creates a new MQTT topic filter as a copy of \a filter.
*/
QMqttTopicFilter::QMqttTopicFilter(const QMqttTopicFilter &filter) : d(filter.d)
{
}
/*!
Destroys the QMqttTopicFilter object.
*/
QMqttTopicFilter::~QMqttTopicFilter()
{
}
/*!
Assigns the MQTT topic filter \a filter to this object, and returns a
reference to the copy.
*/
QMqttTopicFilter &QMqttTopicFilter::operator=(const QMqttTopicFilter &filter)
{
d = filter.d;
return *this;
}
/*!
Returns the topic filter.
*/
QString QMqttTopicFilter::filter() const
{
return d->filter;
}
/*!
Sets the topic filter to \a filter.
*/
void QMqttTopicFilter::setFilter(const QString &filter)
{
d.detach();
d->filter = filter;
}
/*!
\since 5.12
Returns the name of a share if the topic filter has been specified as
a shared subscription. The format of shared subscriptions is defined
as \c $share/sharename/topicfilter.
*/
QString QMqttTopicFilter::sharedSubscriptionName() const
{
QString result;
if (d->filter.startsWith(QLatin1String("$share/"))) {
// Has to have at least two /
// $share/<sharename>/topicfilter
result = d->filter.section(QLatin1Char('/'), 1, 1);
}
return result;
}
/*!
Returns \c true if the topic filter is valid according to the MQTT standard
section 4.7, or \c false otherwise.
*/
bool QMqttTopicFilter::isValid() const
{
// MQTT-4.7.3-1, MQTT-4.7.3-3, and MQTT-4.7.3-2
const int size = d->filter.size();
if (size == 0 || size > 65535 || d->filter.contains(QChar(QChar::Null)))
return false;
if (size == 1)
return true;
// '#' MUST be last and its own level. It MUST NOT appear more than at most once.
const int multiLevelPosition = d->filter.indexOf(QLatin1Char('#'));
if (multiLevelPosition != -1
&& (multiLevelPosition != size - 1 || d->filter.at(size-2) != QLatin1Char('/'))) {
return false;
}
// '+' MAY occur multiple times but MUST be its own level.
int singleLevelPosition = d->filter.indexOf(QLatin1Char('+'));
while (singleLevelPosition != -1) {
if ((singleLevelPosition != 0 && d->filter.at(singleLevelPosition - 1) != QLatin1Char('/'))
|| (singleLevelPosition < size - 1 && d->filter.at(singleLevelPosition + 1) != QLatin1Char('/'))) {
return false;
}
singleLevelPosition = d->filter.indexOf(QLatin1Char('+'), singleLevelPosition + 1);
}
// Shared Subscription syntax
// $share/shareName/TopicFilter -- must have at least 2 '/'
if (d->filter.startsWith(QLatin1String("$share/"))) {
const int index = d->filter.indexOf(QLatin1Char('/'), 7);
if (index == -1 || index == 7)
return false;
}
return true;
}
/*!
Returns \c true if the topic filter matches the topic name \a name
honoring the given \a matchOptions, or \c false otherwise.
*/
bool QMqttTopicFilter::match(const QMqttTopicName &name, MatchOptions matchOptions) const
{
if (!name.isValid() || !isValid())
return false;
const QString topic = name.name();
if (topic == d->filter)
return true;
if (matchOptions.testFlag(WildcardsDontMatchDollarTopicMatchOption)
&& topic.startsWith(QLatin1Char('$'))
&& (d->filter.startsWith(QLatin1Char('+'))
|| d->filter == QLatin1Char('#')
|| d->filter == QLatin1String("/#"))) {
return false;
}
if (d->filter.endsWith(QLatin1Char('#'))) {
QStringView root = QStringView{d->filter}.left(d->filter.size() - 1);
if (root.isEmpty()) // Filter: #
return true;
if (root.endsWith(QLatin1Char('/'))) // '#' also represents the parent level!
root = root.left(root.size() - 1);
const auto filterLevels = root.split(QLatin1Char('/'));
const auto topicLevels = QStringView{topic}.split(QLatin1Char('/'));
if (topicLevels.size() < filterLevels.size())
return false;
for (int i = 0; i < filterLevels.size(); ++i) {
if (filterLevels.at(i) != topicLevels.at(i) && filterLevels.at(i) != QLatin1Char('+'))
return false;
}
return true;
}
if (d->filter.contains(QLatin1Char('+'))) {
const auto filterLevels = QStringView{d->filter}.split(QLatin1Char('/'));
const auto topicLevels = QStringView{topic}.split(QLatin1Char('/'));
if (filterLevels.size() != topicLevels.size())
return false;
for (int i = 0; i < filterLevels.size(); ++i) {
const auto &level = filterLevels.at(i);
if (level != QLatin1Char('+') && level != topicLevels.at(i))
return false;
}
return true;
}
return false;
}
/*!
//! friend
\fn bool QMqttTopicFilter::operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
Returns \c true if the topic filters \a lhs and \a rhs are equal,
otherwise returns \c false.
*/
bool operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW
{
return (lhs.d == rhs.d) || (lhs.d->filter == rhs.d->filter);
}
/*!
//! friend
\fn bool QMqttTopicFilter::operator!=(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
Returns \c true if the topic filters \a lhs and \a rhs are different,
otherwise returns \c false.
*/
/*!
//! friend
\fn bool QMqttTopicFilter::operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
Returns \c true if the topic filter \a lhs is lexically less than the topic
filter \a rhs; otherwise returns \c false.
*/
bool operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW
{
return lhs.d->filter < rhs.d->filter;
}
/*!
\relates QHash
Returns the hash value for \a filter. If specified, \a seed is used to
initialize the hash.
*/
size_t qHash(const QMqttTopicFilter &filter, size_t seed) Q_DECL_NOTHROW
{
return qHash(filter.d->filter, seed);
}
#ifndef QT_NO_DATASTREAM
/*! \relates QMqttTopicFilter
Writes the topic filter \a filter to the stream \a out and returns a
reference to the stream.
\sa{Serializing Qt Data Types}{Format of the QDataStream operators}
*/
QDataStream &operator<<(QDataStream &out, const QMqttTopicFilter &filter)
{
out << filter.filter();
return out;
}
/*! \relates QMqttTopicFilter
Reads a topic filter into \a filter from the stream \a in and returns a
reference to the stream.
\sa{Serializing Qt Data Types}{Format of the QDataStream operators}
*/
QDataStream &operator>>(QDataStream &in, QMqttTopicFilter &filter)
{
QString f;
in >> f;
filter.setFilter(f);
return in;
}
#endif // QT_NO_DATASTREAM
#ifndef QT_NO_DEBUG_STREAM
QDebug operator<<(QDebug d, const QMqttTopicFilter &filter)
{
QDebugStateSaver saver(d);
d.nospace() << "QMqttTopicFilter(" << filter.filter() << ')';
return d;
}
#endif
QT_END_NAMESPACE