Skip to content

Commit cca4173

Browse files
committed
Fix CVE-2026-22591
This commit fixes CVE-2026-22591 by limiting the number of subexpressions in a filter expression. This is a squashed commit of a privately reviewed branch. Signed-off-by: Miguel Company <miguelcompany@eprosima.com> Reviewed-by: Ricardo González Moreno <ricardo@richiware.dev>
1 parent 95ab194 commit cca4173

5 files changed

Lines changed: 433 additions & 45 deletions

File tree

src/cpp/fastdds/domain/DomainParticipantImpl.cpp

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@
5151
#include <fastdds/rtps/writer/WriterDiscoveryStatus.hpp>
5252

5353
#include <fastdds/builtin/type_lookup_service/TypeLookupManager.hpp>
54-
#include <fastdds/core/policy/QosPolicyUtils.hpp>
5554
#include <fastdds/publisher/DataWriterImpl.hpp>
5655
#include <fastdds/publisher/PublisherImpl.hpp>
5756
#include <fastdds/subscriber/SubscriberImpl.hpp>
5857
#include <fastdds/topic/ContentFilteredTopicImpl.hpp>
58+
#include <fastdds/topic/DDSSQLFilter/DDSFilterFactory.hpp>
5959
#include <fastdds/topic/TopicImpl.hpp>
6060
#include <fastdds/topic/TopicProxy.hpp>
6161
#include <fastdds/topic/TopicProxyFactory.hpp>
@@ -94,6 +94,54 @@ using rtps::ReaderDiscoveryStatus;
9494
using rtps::ResourceEvent;
9595
using rtps::WriterDiscoveryStatus;
9696

97+
static size_t get_filter_max_subexpressions(
98+
const DomainParticipantQos& qos)
99+
{
100+
constexpr const char parameter_name[] = "dds.sql.expression.max_subexpressions";
101+
const std::string* property = fastdds::rtps::PropertyPolicyHelper::find_property(
102+
qos.properties(), parameter_name);
103+
if (nullptr != property)
104+
{
105+
try
106+
{
107+
return std::stoul(*property);
108+
}
109+
catch (...)
110+
{
111+
EPROSIMA_LOG_WARNING(DOMAIN_PARTICIPANT,
112+
"Invalid value for dds.sql.expression.max_subexpressions property: "
113+
<< *property << ". Will use default value of "
114+
<< DDSSQLFilter::DDSFilterFactory::DEFAULT_MAX_SUBEXPRESSIONS);
115+
}
116+
}
117+
118+
return DDSSQLFilter::DDSFilterFactory::DEFAULT_MAX_SUBEXPRESSIONS;
119+
}
120+
121+
static size_t get_filter_max_expression_length(
122+
const DomainParticipantQos& qos)
123+
{
124+
constexpr const char parameter_name[] = "dds.sql.expression.max_expression_length";
125+
const std::string* property = fastdds::rtps::PropertyPolicyHelper::find_property(
126+
qos.properties(), parameter_name);
127+
if (nullptr != property)
128+
{
129+
try
130+
{
131+
return std::stoul(*property);
132+
}
133+
catch (...)
134+
{
135+
EPROSIMA_LOG_WARNING(DOMAIN_PARTICIPANT,
136+
"Invalid value for dds.sql.expression.max_expression_length property: "
137+
<< *property << ". Will use default value of "
138+
<< DDSSQLFilter::DDSFilterFactory::DEFAULT_MAX_EXPRESSION_LENGTH);
139+
}
140+
}
141+
142+
return DDSSQLFilter::DDSFilterFactory::DEFAULT_MAX_EXPRESSION_LENGTH;
143+
}
144+
97145
DomainParticipantImpl::DomainParticipantImpl(
98146
DomainParticipant* dp,
99147
DomainId_t did,
@@ -107,6 +155,7 @@ DomainParticipantImpl::DomainParticipantImpl(
107155
, listener_(listen)
108156
, default_pub_qos_(PUBLISHER_QOS_DEFAULT)
109157
, default_sub_qos_(SUBSCRIBER_QOS_DEFAULT)
158+
, dds_sql_filter_factory_(get_filter_max_subexpressions(qos), get_filter_max_expression_length(qos))
110159
, default_topic_qos_(TOPIC_QOS_DEFAULT)
111160
, id_counter_(0)
112161
#pragma warning (disable : 4355 )

src/cpp/fastdds/topic/DDSSQLFilter/DDSFilterFactory.cpp

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -548,59 +548,69 @@ ReturnCode_t DDSFilterFactory::create_content_filter(
548548
}
549549
}
550550
}
551-
else if (std::strlen(filter_expression) == 0)
552-
{
553-
delete_content_filter(filter_class_name, filter_instance);
554-
filter_instance = &empty_expression_;
555-
ret = RETCODE_OK;
556-
}
557551
else
558552
{
559-
std::shared_ptr<xtypes::TypeObject> type_object = get_complete_type_object(type_name, data_type);
553+
size_t expression_len = std::strlen(filter_expression);
560554

561-
if (!type_object)
555+
if (0 == expression_len)
562556
{
563-
EPROSIMA_LOG_ERROR(DDSSQLFILTER, "No TypeObject found for type " << type_name);
557+
delete_content_filter(filter_class_name, filter_instance);
558+
filter_instance = &empty_expression_;
559+
ret = RETCODE_OK;
564560
}
565-
else
561+
else if (validate_filter_expression(filter_expression, expression_len))
566562
{
567-
auto node = parser::parse_filter_expression(filter_expression, type_object);
568-
if (node)
563+
std::shared_ptr<xtypes::TypeObject> type_object = get_complete_type_object(type_name, data_type);
564+
565+
if (!type_object)
569566
{
570-
DynamicType::_ref_type dyn_type = DynamicTypeBuilderFactory::get_instance()->create_type_w_type_object(
571-
*type_object)->build();
572-
if (dyn_type)
567+
EPROSIMA_LOG_ERROR(DDSSQLFILTER, "No TypeObject found for type " << type_name);
568+
ret = RETCODE_PRECONDITION_NOT_MET;
569+
}
570+
else
571+
{
572+
auto node = parser::parse_filter_expression(filter_expression, type_object);
573+
if (node)
573574
{
574-
DDSFilterExpression* expr = get_expression();
575-
expr->set_type(dyn_type);
576-
size_t n_params = filter_parameters.length();
577-
expr->parameters.reserve(n_params);
578-
while (expr->parameters.size() < n_params)
575+
DynamicTypeBuilderFactory::_ref_type factory = DynamicTypeBuilderFactory::get_instance();
576+
DynamicType::_ref_type dyn_type = factory->create_type_w_type_object(*type_object)->build();
577+
if (dyn_type)
579578
{
580-
expr->parameters.emplace_back();
581-
}
582-
ExpressionParsingState state{ std::make_shared<xtypes::TypeObject>(*type_object),
583-
filter_parameters, expr };
584-
ret = convert_tree<DDSFilterCondition>(state, expr->root, *(node->children[0]));
585-
if (RETCODE_OK == ret)
586-
{
587-
delete_content_filter(filter_class_name, filter_instance);
588-
filter_instance = expr;
579+
DDSFilterExpression* expr = get_expression();
580+
expr->set_type(dyn_type);
581+
size_t n_params = filter_parameters.length();
582+
expr->parameters.reserve(n_params);
583+
while (expr->parameters.size() < n_params)
584+
{
585+
expr->parameters.emplace_back();
586+
}
587+
ExpressionParsingState state{ std::make_shared<xtypes::TypeObject>(*type_object),
588+
filter_parameters, expr };
589+
ret = convert_tree<DDSFilterCondition>(state, expr->root, *(node->children[0]));
590+
if (RETCODE_OK == ret)
591+
{
592+
delete_content_filter(filter_class_name, filter_instance);
593+
filter_instance = expr;
594+
}
595+
else
596+
{
597+
delete_content_filter(filter_class_name, expr);
598+
}
589599
}
590600
else
591601
{
592-
delete_content_filter(filter_class_name, expr);
602+
ret = RETCODE_BAD_PARAMETER;
593603
}
594604
}
595605
else
596606
{
597607
ret = RETCODE_BAD_PARAMETER;
598608
}
599609
}
600-
else
601-
{
602-
ret = RETCODE_BAD_PARAMETER;
603-
}
610+
}
611+
else
612+
{
613+
ret = RETCODE_BAD_PARAMETER;
604614
}
605615
}
606616

@@ -627,6 +637,50 @@ ReturnCode_t DDSFilterFactory::delete_content_filter(
627637
return RETCODE_OK;
628638
}
629639

640+
bool DDSFilterFactory::validate_filter_expression(
641+
const char* expression,
642+
const size_t expression_len) const
643+
{
644+
if (expression_len > max_expression_length_)
645+
{
646+
// Expression too long
647+
return false;
648+
}
649+
650+
// Check parentheses balance and count
651+
size_t num_parentheses = 0;
652+
size_t pending_size = expression_len;
653+
for (const char* ptr = expression; (pending_size > 0) && (*ptr != '\0'); ++ptr, --pending_size)
654+
{
655+
if (*ptr == '(')
656+
{
657+
++num_parentheses;
658+
659+
if (num_parentheses > max_subexpressions_)
660+
{
661+
// Too many nested parentheses
662+
return false;
663+
}
664+
}
665+
else if (*ptr == ')')
666+
{
667+
if (num_parentheses == 0)
668+
{
669+
// Unmatched closing parenthesis
670+
return false;
671+
}
672+
--num_parentheses;
673+
}
674+
}
675+
if (num_parentheses != 0)
676+
{
677+
// Unmatched opening parenthesis
678+
return false;
679+
}
680+
681+
return true;
682+
}
683+
630684
} // namespace DDSSQLFilter
631685
} // namespace dds
632686
} // namespace fastdds

src/cpp/fastdds/topic/DDSSQLFilter/DDSFilterFactory.hpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ class DDSFilterFactory final : public IContentFilterFactory
4040

4141
public:
4242

43+
static constexpr size_t DEFAULT_MAX_SUBEXPRESSIONS = 256;
44+
static constexpr size_t DEFAULT_MAX_EXPRESSION_LENGTH = 16 * 1024;
45+
46+
explicit DDSFilterFactory(
47+
size_t max_subexpressions,
48+
size_t max_expression_length)
49+
: max_subexpressions_(max_subexpressions)
50+
, max_expression_length_(max_expression_length)
51+
{
52+
}
53+
4354
~DDSFilterFactory();
4455

4556
ReturnCode_t create_content_filter(
@@ -56,6 +67,20 @@ class DDSFilterFactory final : public IContentFilterFactory
5667

5768
private:
5869

70+
/**
71+
* @brief Validate a DDS-SQL filter expression.
72+
*
73+
* Performs basic validation checks on the expression before passing it to the PEGTL parser.
74+
*
75+
* @param[in] expression The filter expression to validate.
76+
* @param[in] expression_len The length of the filter expression.
77+
*
78+
* @return true if the expression is valid, false otherwise.
79+
*/
80+
bool validate_filter_expression(
81+
const char* expression,
82+
const size_t expression_len) const;
83+
5984
/**
6085
* Retrieve a DDSFilterExpression from the pool.
6186
*
@@ -86,6 +111,10 @@ class DDSFilterFactory final : public IContentFilterFactory
86111
DDSFilterEmptyExpression empty_expression_;
87112
/// Pool of DDSFilterExpression objects
88113
ObjectPool<DDSFilterExpression*> expression_pool_;
114+
/// Maximum number of sub-expressions allowed in a filter expression
115+
const size_t max_subexpressions_ = DEFAULT_MAX_SUBEXPRESSIONS;
116+
/// Maximum length of a filter expression
117+
const size_t max_expression_length_ = DEFAULT_MAX_EXPRESSION_LENGTH;
89118

90119
};
91120

0 commit comments

Comments
 (0)