From: Vsevolod Stakhov Date: Thu, 23 Jul 2026 10:29:42 +0000 (+0100) Subject: [Fix] html: bound attributes per tag and per task X-Git-Tag: 4.1.3~38 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=b6111378f2dcae520cb08d163abeb4e4ffb567d0;p=thirdparty%2Frspamd.git [Fix] html: bound attributes per tag and per task --- diff --git a/src/libserver/html/html.cxx b/src/libserver/html/html.cxx index 1d68e53b03..7af38a9763 100644 --- a/src/libserver/html/html.cxx +++ b/src/libserver/html/html.cxx @@ -47,7 +47,9 @@ namespace rspamd::html { -static const unsigned int max_tags = 8192; /* Ignore tags if this maximum is reached */ +static const unsigned int max_tags = 8192; /* Ignore tags if this maximum is reached */ +static const unsigned int max_attrs_per_tag = 128; /* Ignore attributes of a single tag if this maximum is reached */ +static const unsigned int max_attrs_per_task = 65536; /* Ignore attributes in all HTML parts of a task if this maximum is reached */ static const html_tags_storage html_tags_defs; @@ -919,9 +921,10 @@ enum tag_parser_state { struct tag_content_parser_state { tag_parser_state cur_state = parse_start; std::string buf; - std::string attr_name; // Store current attribute name - const char *value_start = nullptr;// Track where attribute value starts in input - const char *html_start = nullptr; // Base pointer to HTML buffer start + std::string attr_name; // Store current attribute name + const char *value_start = nullptr; // Track where attribute value starts in input + const char *html_start = nullptr; // Base pointer to HTML buffer start + unsigned int *attrs_count = nullptr;// Task-global attributes counter (task pool variable) void reset() { @@ -947,6 +950,27 @@ html_parse_tag_content(rspamd_mempool_t *pool, */ auto store_component_value = [&]() -> void { if (!parser_env.attr_name.empty()) { + /* + * Enforce both per-tag and task-global attribute budgets: a single + * tag must not evade the tags limit by cramming everything into + * attributes + */ + if (tag->components.size() >= max_attrs_per_tag || + *parser_env.attrs_count >= max_attrs_per_task) { + if (!(hc->flags & RSPAMD_HTML_FLAG_TOO_MANY_ATTRS)) { + msg_warn_pool("too many attributes in HTML tags; ignoring the rest"); + hc->flags |= RSPAMD_HTML_FLAG_TOO_MANY_ATTRS; + } + + parser_env.buf.clear(); + parser_env.attr_name.clear(); + parser_env.value_start = nullptr; + + return; + } + + (*parser_env.attrs_count)++; + std::string_view attr_name_view, value_view; // Store attribute name in persistent memory @@ -2550,6 +2574,20 @@ auto html_process_input(struct rspamd_task *task, start = c; content_parser_env.html_start = start;// Initialize for span tracking + /* + * Attributes budget is task-global, as a task can have multiple HTML parts; + * the counter is stashed in the task pool to survive across parts + */ + static const char attrs_count_var[] = "html_attrs_count"; + auto *task_attrs_count = static_cast(rspamd_mempool_get_variable(pool, attrs_count_var)); + + if (task_attrs_count == nullptr) { + task_attrs_count = rspamd_mempool_alloc0_type(pool, unsigned int); + rspamd_mempool_set_variable(pool, attrs_count_var, task_attrs_count, nullptr); + } + + content_parser_env.attrs_count = task_attrs_count; + while (p < end) { t = *p; diff --git a/src/libserver/html/html.h b/src/libserver/html/html.h index 50dfcc1c66..1c286bc915 100644 --- a/src/libserver/html/html.h +++ b/src/libserver/html/html.h @@ -38,6 +38,7 @@ extern "C" { #define RSPAMD_HTML_FLAG_TOO_MANY_TAGS (1 << 6) #define RSPAMD_HTML_FLAG_HAS_DATA_URLS (1 << 7) #define RSPAMD_HTML_FLAG_HAS_ZEROS (1 << 8) +#define RSPAMD_HTML_FLAG_TOO_MANY_ATTRS (1 << 9) /* * Image flags diff --git a/src/libserver/html/html_tests.cxx b/src/libserver/html/html_tests.cxx index e3ab6e5693..c029d2129e 100644 --- a/src/libserver/html/html_tests.cxx +++ b/src/libserver/html/html_tests.cxx @@ -344,6 +344,93 @@ TEST_SUITE("html") g_byte_array_free(tmp, TRUE); rspamd_mempool_delete(pool); } + + TEST_CASE("html attributes limits") + { + /* Must match max_attrs_per_tag and max_attrs_per_task in html.cxx */ + constexpr unsigned int max_attrs_per_tag = 128; + constexpr unsigned int max_attrs_per_task = 65536; + + rspamd_url_init(NULL); + auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(), + "html", 0); + struct rspamd_task fake_task; + memset(&fake_task, 0, sizeof(fake_task)); + fake_task.task_pool = pool; + + auto process = [&](const std::string &input) -> html_content * { + GByteArray *tmp = g_byte_array_sized_new(input.size()); + g_byte_array_append(tmp, (const uint8_t *) input.data(), input.size()); + auto *hc = html_process_input(&fake_task, tmp, nullptr, nullptr, nullptr, true, nullptr); + g_byte_array_free(tmp, TRUE); + return hc; + }; + + auto find_tag = [](html_content *hc, tag_id_t id) -> const html_tag * { + const html_tag *found = nullptr; + hc->traverse_all_tags([&](const html_tag *t) -> bool { + if (t->id == id) { + found = t; + return false; + } + return true; + }); + return found; + }; + + SUBCASE("per-tag budget") + { + /* A single tag with twice the per-tag attributes budget */ + std::string input = "flags & RSPAMD_HTML_FLAG_TOO_MANY_ATTRS) != 0); + + const auto *atag = find_tag(hc, Tag_A); + CHECK(atag != nullptr); + CHECK(atag->components.size() == max_attrs_per_tag); + /* The first attribute must survive */ + CHECK(atag->find_component_by_name("href").has_value()); + } + + SUBCASE("task-global budget spans parts") + { + /* Exhaust the task budget exactly in the first part */ + constexpr unsigned int ntags = max_attrs_per_task / max_attrs_per_tag; + std::string input; + input.reserve(ntags * (max_attrs_per_tag * 8 + 16)); + for (unsigned int i = 0; i < ntags; i++) { + input += "flags & RSPAMD_HTML_FLAG_TOO_MANY_ATTRS) == 0); + const auto *stag = find_tag(hc, Tag_SPAN); + CHECK(stag != nullptr); + CHECK(stag->components.size() == max_attrs_per_tag); + + /* The second part of the same task gets no attributes at all */ + auto *hc2 = process("link"); + CHECK(hc2 != nullptr); + CHECK((hc2->flags & RSPAMD_HTML_FLAG_TOO_MANY_ATTRS) != 0); + const auto *atag = find_tag(hc2, Tag_A); + CHECK(atag != nullptr); + CHECK(atag->components.empty()); + } + + rspamd_mempool_delete(pool); + } } } /* namespace rspamd::html */