]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] html: bound attributes per tag and per task
authorVsevolod Stakhov <vsevolod@rspamd.com>
Thu, 23 Jul 2026 10:29:42 +0000 (11:29 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Thu, 23 Jul 2026 10:29:42 +0000 (11:29 +0100)
src/libserver/html/html.cxx
src/libserver/html/html.h
src/libserver/html/html_tests.cxx

index 1d68e53b03d601e61c548438589f93f0782a2342..7af38a9763485bad7b3cd9f4f15b3355acf73592 100644 (file)
@@ -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<unsigned int *>(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;
 
index 50dfcc1c66950d1d9bc218cf379ed69255eefd5f..1c286bc91595a13d613fb1e9ef033d3b5aba03ff 100644 (file)
@@ -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
index e3ab6e5693b0ac6986d59a4f0047cf342099529a..c029d2129efda53e61f1c2a4cfb7190c23def46a 100644 (file)
@@ -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 = "<a href=\"http://example.com\"";
+                       for (unsigned int i = 0; i < max_attrs_per_tag * 2; i++) {
+                               input += fmt::format(" data-x{}=\"{}\"", i, i);
+                       }
+                       input += ">link</a>";
+
+                       auto *hc = process(input);
+                       CHECK(hc != nullptr);
+                       CHECK((hc->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 += "<span";
+                               for (unsigned int j = 0; j < max_attrs_per_tag; j++) {
+                                       input += " a=\"1\"";
+                               }
+                               input += ">x</span>";
+                       }
+
+                       auto *hc = process(input);
+                       CHECK(hc != nullptr);
+                       /* Budget is reached, but nothing is dropped yet */
+                       CHECK((hc->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("<a href=\"http://example.com\">link</a>");
+                       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 */