From: Vsevolod Stakhov Date: Wed, 22 Jul 2026 12:20:05 +0000 (+0100) Subject: [Fix] images: avoid quadratic Content-ID linking X-Git-Tag: 4.1.3~48 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d5c48178e36d3ccc3cdd96c94a55860de86b4009;p=thirdparty%2Frspamd.git [Fix] images: avoid quadratic Content-ID linking --- diff --git a/src/libmime/images.c b/src/libmime/images.c index fd3f5cf315..678b0f292a 100644 --- a/src/libmime/images.c +++ b/src/libmime/images.c @@ -19,13 +19,6 @@ #include "message.h" #include "libserver/html/html.h" -#define msg_debug_images(...) rspamd_conditional_debug_fast(NULL, NULL, \ - rspamd_images_log_id, "images", task->task_pool->tag.uid, \ - G_STRFUNC, \ - __VA_ARGS__) - -INIT_LOG_MODULE(images) - #ifdef USABLE_GD #include "gd.h" #include "hash.h" @@ -674,13 +667,13 @@ rspamd_image_type_str(enum rspamd_image_type type) } static void -rspamd_image_process_part(struct rspamd_task *task, struct rspamd_mime_part *part) +rspamd_image_add_content_id(struct rspamd_task *task, + struct rspamd_mime_part *part, + GHashTable *cid_images) { struct rspamd_mime_header *rh; - struct rspamd_mime_text_part *tp; - struct html_image *himg; const char *cid; - unsigned int cid_len, i; + gsize cid_len; struct rspamd_image *img; img = (struct rspamd_image *) part->specific.img; @@ -704,28 +697,9 @@ rspamd_image_process_part(struct rspamd_task *task, struct rspamd_mime_part *par cid_len--; } - PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, tp) - { - if (IS_TEXT_PART_HTML(tp) && tp->html != NULL) { - himg = rspamd_html_find_embedded_image(tp->html, cid, cid_len); - - if (himg != NULL) { - img->html_image = himg; - himg->embedded_image = img; - - msg_debug_images("found linked image by cid: <%s>", - cid); - - if (himg->height == 0) { - himg->height = img->height; - } - - if (himg->width == 0) { - himg->width = img->width; - } - } - } - } + char *cid_copy = rspamd_mempool_strdup_len(task->task_pool, + cid, cid_len); + g_hash_table_replace(cid_images, cid_copy, img); } } } @@ -734,12 +708,27 @@ rspamd_image_process_part(struct rspamd_task *task, struct rspamd_mime_part *par void rspamd_images_link(struct rspamd_task *task) { struct rspamd_mime_part *part; + struct rspamd_mime_text_part *tp; unsigned int i; + GHashTable *cid_images; + + cid_images = g_hash_table_new(g_str_hash, g_str_equal); PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, part) { if (part->part_type == RSPAMD_MIME_PART_IMAGE) { - rspamd_image_process_part(task, part); + rspamd_image_add_content_id(task, part, cid_images); + } + } + + if (g_hash_table_size(cid_images) > 0) { + PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, tp) + { + if (IS_TEXT_PART_HTML(tp) && tp->html != NULL) { + rspamd_html_link_embedded_images(tp->html, cid_images); + } } } + + g_hash_table_destroy(cid_images); } diff --git a/src/libserver/html/html.cxx b/src/libserver/html/html.cxx index 675ec7eeb6..7a7116050a 100644 --- a/src/libserver/html/html.cxx +++ b/src/libserver/html/html.cxx @@ -3375,6 +3375,33 @@ rspamd_html_find_embedded_image(void *html_content, return nullptr; } +void rspamd_html_link_embedded_images(void *html_content, + GHashTable *cid_images) +{ + auto *hc = rspamd::html::html_content::from_ptr(html_content); + + for (auto *html_image: hc->images) { + if (html_image->flags & RSPAMD_HTML_FLAG_IMAGE_EMBEDDED && + html_image->src != nullptr) { + auto *parsed_image = static_cast( + g_hash_table_lookup(cid_images, html_image->src)); + + if (parsed_image != nullptr) { + parsed_image->html_image = html_image; + html_image->embedded_image = parsed_image; + + if (html_image->height == 0) { + html_image->height = parsed_image->height; + } + + if (html_image->width == 0) { + html_image->width = parsed_image->width; + } + } + } + } +} + bool rspamd_html_get_parsed_content(void *html_content, rspamd_ftok_t *dest) { auto *hc = rspamd::html::html_content::from_ptr(html_content); diff --git a/src/libserver/html/html.h b/src/libserver/html/html.h index f256aae9dc..50dfcc1c66 100644 --- a/src/libserver/html/html.h +++ b/src/libserver/html/html.h @@ -115,6 +115,14 @@ const char *rspamd_html_tag_name(void *tag, gsize *len); struct html_image *rspamd_html_find_embedded_image(void *html_content, const char *cid, gsize cid_len); +/** + * Link all embedded HTML images using a Content-ID to rspamd_image hash table + * @param html_content + * @param cid_images + */ +void rspamd_html_link_embedded_images(void *html_content, + GHashTable *cid_images); + /** * Stores parsed content in ftok_t structure * @param html_content diff --git a/test/lua/unit/task.lua b/test/lua/unit/task.lua index 98687ea7af..75badf5ed0 100644 --- a/test/lua/unit/task.lua +++ b/test/lua/unit/task.lua @@ -330,6 +330,39 @@ Thank you, task:destroy() end) + test("Embedded image dimensions are linked by Content-ID", function() + local png = "\137PNG\r\n\26\n" .. + "\0\0\0\13IHDR" .. + "\0\0\1\64\0\0\0\240" + local msg = table.concat { + hdrs, + "Content-Type: multipart/related; boundary=LINKED-IMAGE\n", + "\n", + "--LINKED-IMAGE\n", + "Content-Type: text/html\n", + "\n", + "\n", + "--LINKED-IMAGE\n", + "Content-Type: image/png\n", + "Content-ID: \n", + "Content-Transfer-Encoding: binary\n", + "\n", + png, + "\n--LINKED-IMAGE--\n", + } + local res, task = rspamd_task.load_from_string(msg, rspamd_config) + assert_true(res, "failed to load message") + task:process_message() + + local text_parts = task:get_text_parts() + assert_equal(1, #text_parts) + local html_images = text_parts[1]:get_html():get_images() + assert_equal(1, #html_images) + assert_equal(320, html_images[1].width) + assert_equal(240, html_images[1].height) + task:destroy() + end) + test("Part URLs are not deduplicated across MIME parts", function() local msg = table.concat { hdrs, diff --git a/test/rspamd_cxx_unit.cxx b/test/rspamd_cxx_unit.cxx index f824d1f0b2..ee9a71d089 100644 --- a/test/rspamd_cxx_unit.cxx +++ b/test/rspamd_cxx_unit.cxx @@ -28,6 +28,7 @@ #include "rspamd_cxx_unit_rfc2047.hxx" #include "rspamd_cxx_unit_html_url_rewrite.hxx" #include "rspamd_cxx_unit_html_cta.hxx" +#include "rspamd_cxx_unit_images.hxx" #include "rspamd_cxx_unit_upstream_token_bucket.hxx" #include "rspamd_cxx_unit_upstream_ring_hash.hxx" #include "rspamd_cxx_unit_upstream_round_robin.hxx" diff --git a/test/rspamd_cxx_unit_images.hxx b/test/rspamd_cxx_unit_images.hxx new file mode 100644 index 0000000000..4a14525434 --- /dev/null +++ b/test/rspamd_cxx_unit_images.hxx @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RSPAMD_RSPAMD_CXX_UNIT_IMAGES_HXX +#define RSPAMD_RSPAMD_CXX_UNIT_IMAGES_HXX + +#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#include "doctest/doctest.h" + +#include "libmime/images.h" +#include "libserver/html/html.h" +#include "libserver/html/html.hxx" +#include "libutil/mem_pool.h" + +#include + +TEST_SUITE("image_processing") +{ + TEST_CASE("bulk Content-ID linking covers repeated HTML images") + { + auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(), nullptr, 0); + std::string html; + html.reserve(1024 * 32); + + for (unsigned int i = 0; i < 1024; i++) { + html += ""; + } + + auto *input = g_byte_array_sized_new(html.size()); + g_byte_array_append(input, + reinterpret_cast(html.data()), html.size()); + auto *parsed = rspamd_html_process_part(pool, input); + g_byte_array_free(input, TRUE); + + struct rspamd_image mime_image = {}; + mime_image.width = 320; + mime_image.height = 240; + auto *cid_images = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(cid_images, + const_cast("shared-image"), &mime_image); + + rspamd_html_link_embedded_images(parsed, cid_images); + + auto *hc = rspamd::html::html_content::from_ptr(parsed); + REQUIRE(hc->images.size() == 1024); + for (const auto *html_image: hc->images) { + CHECK(html_image->embedded_image == &mime_image); + CHECK(html_image->width == mime_image.width); + CHECK(html_image->height == mime_image.height); + } + + g_hash_table_destroy(cid_images); + rspamd_mempool_delete(pool); + } +} + +#endif