From 527bd69129e2481e5c4488ebb0eb350400c99cee Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Wed, 26 Aug 2026 16:46:10 +0200 Subject: [PATCH] Reject unterminated wildcard character classes in filter allocation --- cmdline/elem.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ cmdline/selftest.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/cmdline/elem.c b/cmdline/elem.c index ebabfc1a..e1f67bda 100644 --- a/cmdline/elem.c +++ b/cmdline/elem.c @@ -31,6 +31,38 @@ void content_free(void* void_content) free(content); } +/** + * Check that all wildcard character classes are terminated. + * + * Escaped '[' characters are literals and don't start a class. + * Escape characters inside a class are literals, consistently with + * match_class(). + */ +static int filter_pattern_class_valid(const char* pattern) +{ + const char* i; + int in_class = 0; + + for (i = pattern; *i; ++i) { + if (in_class) { + if (*i == ']') + in_class = 0; + } else { +#ifdef _WIN32 + if (*i == '^' && i[1] != 0) { +#else + if (*i == '\\' && i[1] != 0) { +#endif + ++i; + } else if (*i == '[') { + in_class = 1; + } + } + } + + return !in_class; +} + struct snapraid_filter* filter_alloc_file(int direction, const char* root, const char* pattern) { struct snapraid_filter* filter; @@ -46,6 +78,12 @@ struct snapraid_filter* filter_alloc_file(int direction, const char* root, const pathimport(filter->root, sizeof(filter->root), root); filter->direction = direction; + /* reject unterminated wildcard character classes */ + if (!filter_pattern_class_valid(filter->pattern)) { + free(filter); + return 0; + } + /* reject the root directory and standalone absolute directory wildcard as invalid patterns */ if (pathcmp(filter->pattern, "/") == 0 || pathcmp(filter->pattern, "/**/") == 0) { free(filter); @@ -128,6 +166,12 @@ struct snapraid_filter* filter_alloc_disk(int direction, const char* pattern) pathimport(filter->pattern, sizeof(filter->pattern), pattern); filter->direction = direction; + /* reject unterminated wildcard character classes */ + if (!filter_pattern_class_valid(filter->pattern)) { + free(filter); + return 0; + } + /* it's a disk filter */ filter->is_disk = 1; filter->is_abs = 0; diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 734f4c98..c6d33e42 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -1031,6 +1031,34 @@ struct { { "foo/.hidden", 1 }, { "foo/..hidden", 1 }, + /* invalid unterminated character classes */ + { "[", 0 }, + { "foo[", 0 }, + { "[abc", 0 }, + { "foo/[ab", 0 }, + { "/foo/[ab/bar", 0 }, + { "foo/[a-z", 0 }, + + /* valid character classes */ + { "[abc]", 1 }, + { "foo[abc]", 1 }, + { "foo/[ab]/bar", 1 }, + { "foo/[a-z]/bar", 1 }, + +#ifdef _WIN32 + /* escaped '[' is literal */ + { "foo^[bar", 1 }, + + /* escaped '^' followed by '[' starts a class */ + { "foo^^[bar", 0 }, +#else + /* escaped '[' is literal */ + { "foo\\[bar", 1 }, + + /* escaped '\' followed by '[' starts a class */ + { "foo\\\\[bar", 0 }, +#endif + { 0, 0 } }; @@ -1043,6 +1071,9 @@ static const struct { { "disk", 1 }, { "disk?", 1 }, { "data*", 1 }, + { "[ab", 0 }, + { "disk[12", 0 }, + { "disk[12]", 1 }, { 0, 0 } };