]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] map: bound remote HTTP map sizes, compressed and decompressed
authorVsevolod Stakhov <vsevolod@rspamd.com>
Thu, 23 Jul 2026 11:39:06 +0000 (12:39 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Thu, 23 Jul 2026 11:39:06 +0000 (12:39 +0100)
Remote HTTP maps had no size ceiling: the map client connections never
called rspamd_http_connection_set_max_size, so a map server could feed
an arbitrarily large body that is fully retained in shared memory, and
the zstd path allocated the output buffer straight from the
frame-advertised decompressed size, then doubled it without limit, so a
small zstd bomb could balloon memory indefinitely.

Add a max_map_size option (256Mb by default, 0 disables) applied as the
HTTP body limit on both map client connections, reject frames
advertising a larger decompressed size upfront and enforce the same
ceiling in the streaming growth loop. The limit can be overridden per
map with max_size in the maps { } block, like the existing timeout and
keepalive knobs.

src/libserver/cfg_file.h
src/libserver/cfg_rcl.cxx
src/libserver/cfg_utils.cxx
src/libserver/maps/map.c
src/libserver/maps/map_private.h

index a14624ce8a358f6123c21d2820efb4c105937a92..3c7f357f816d3cc54ff03269518b7590f0f9b4af 100644 (file)
@@ -446,6 +446,7 @@ struct rspamd_config {
        double map_timeout;               /**< maps watch timeout                                                                       */
        double map_file_watch_multiplier; /**< multiplier for watch timeout when maps are files */
        char *maps_cache_dir;             /**< where to save HTTP cached data                                           */
+       gsize max_map_size;               /**< maximum size of a map, compressed or expanded            */
 
        double monitored_interval;  /**< interval between monitored checks                                      */
        gboolean disable_monitored; /**< disable monitoring completely                                          */
index e3b08c5abd628a2137bcb83b25db7afd497e5ad8..5583524c6a335852482f894e05be6bf2a5884dfd 100644 (file)
@@ -2074,6 +2074,12 @@ rspamd_rcl_config_init(struct rspamd_config *cfg, GHashTable *skip_sections)
                                                                           G_STRUCT_OFFSET(struct rspamd_config, maps_cache_dir),
                                                                           0,
                                                                           "Directory to save maps cached data (default: $DBDIR)");
+               rspamd_rcl_add_default_handler(sub,
+                                                                          "max_map_size",
+                                                                          rspamd_rcl_parse_struct_integer,
+                                                                          G_STRUCT_OFFSET(struct rspamd_config, max_map_size),
+                                                                          RSPAMD_CL_FLAG_INT_SIZE,
+                                                                          "Maximum size of a map, compressed or expanded (256Mb by default, 0 to disable the limit)");
                rspamd_rcl_add_default_handler(sub,
                                                                           "monitoring_watch_interval",
                                                                           rspamd_rcl_parse_struct_time,
index 420cc70ab38af7f04e371d179dd9fc508a0c580e..1c5ebd03c39cb7408720a7583eebd65cd49d7aae 100644 (file)
@@ -87,6 +87,7 @@
 #define DEFAULT_RLIMIT_MAXCORE 0
 #define DEFAULT_MAP_TIMEOUT 60.0 * 5
 #define DEFAULT_MAP_FILE_WATCH_MULTIPLIER 1
+#define DEFAULT_MAX_MAP_SIZE (256 * 1024 * 1024)
 #define DEFAULT_MIN_WORD 0
 #define DEFAULT_MAX_WORD 40
 #define DEFAULT_WORDS_DECAY 600
@@ -291,6 +292,7 @@ rspamd_config_new(enum rspamd_config_init_flags flags)
 
        cfg->map_timeout = DEFAULT_MAP_TIMEOUT;
        cfg->map_file_watch_multiplier = DEFAULT_MAP_FILE_WATCH_MULTIPLIER;
+       cfg->max_map_size = DEFAULT_MAX_MAP_SIZE;
 
        cfg->log_level = G_LOG_LEVEL_WARNING;
        cfg->log_flags = RSPAMD_LOG_FLAG_DEFAULT;
index bd5bb248e4719af65f65c0ae8e3b77516cb26291..24d1dca996cc7fa5f0e343d777d999698e91c0b0 100644 (file)
@@ -586,6 +586,16 @@ rspamd_map_payload_is_zstd(const unsigned char *p, gsize len)
        return FALSE;
 }
 
+static inline gsize
+rspamd_map_effective_max_size(struct http_callback_data *cbd)
+{
+       if (cbd->data->max_size > 0) {
+               return cbd->data->max_size;
+       }
+
+       return cbd->map->cfg ? cbd->map->cfg->max_map_size : 0;
+}
+
 static void
 rspamd_map_try_load_secretbox_key(struct rspamd_config *cfg,
                                                                  struct rspamd_map_backend *bk)
@@ -793,9 +803,7 @@ http_map_finish(struct rspamd_http_connection *conn,
                        ZSTD_inBuffer zin;
                        ZSTD_outBuffer zout;
                        gsize outlen, r;
-
-                       zstream = ZSTD_createDStream();
-                       ZSTD_initDStream(zstream);
+                       gsize max_size = rspamd_map_effective_max_size(cbd);
 
                        zin.pos = 0;
                        zin.src = payload;
@@ -804,6 +812,21 @@ http_map_finish(struct rspamd_http_connection *conn,
                        if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) {
                                outlen = ZSTD_DStreamOutSize();
                        }
+                       else if (max_size > 0 && outlen > max_size) {
+                               msg_err_map("%s(%s): declared decompressed size %z exceeds max_map_size %z",
+                                                       cbd->bk->uri,
+                                                       rspamd_inet_address_to_string_pretty(cbd->addr),
+                                                       outlen, max_size);
+                               if (cbd->bk->is_encrypted && payload && payload != (unsigned char *) in) {
+                                       rspamd_explicit_memzero(payload, payload_len);
+                                       g_free(payload);
+                               }
+                               MAP_RELEASE(cbd->shmem_data, "shmem_data");
+                               goto err;
+                       }
+
+                       zstream = ZSTD_createDStream();
+                       ZSTD_initDStream(zstream);
 
                        final_out = g_malloc(outlen);
 
@@ -831,7 +854,27 @@ http_map_finish(struct rspamd_http_connection *conn,
 
                                if (zout.pos == zout.size) {
                                        /* We need to extend output buffer */
+                                       if (max_size > 0 && zout.size >= max_size) {
+                                               msg_err_map("%s(%s): decompressed data exceeds max_map_size %z",
+                                                                       cbd->bk->uri,
+                                                                       rspamd_inet_address_to_string_pretty(cbd->addr),
+                                                                       max_size);
+                                               ZSTD_freeDStream(zstream);
+                                               g_free(final_out);
+                                               if (cbd->bk->is_encrypted && payload && payload != (unsigned char *) in) {
+                                                       rspamd_explicit_memzero(payload, payload_len);
+                                                       g_free(payload);
+                                               }
+                                               MAP_RELEASE(cbd->shmem_data, "shmem_data");
+                                               goto err;
+                                       }
+
                                        zout.size = zout.size * 2 + 1.0;
+
+                                       if (max_size > 0 && zout.size > max_size) {
+                                               zout.size = max_size;
+                                       }
+
                                        final_out = g_realloc(zout.dst, zout.size);
                                        zout.dst = final_out;
                                }
@@ -1751,6 +1794,11 @@ rspamd_map_dns_callback(struct rdns_reply *reply, void *arg)
                                                                                                          cbd->addr);
 
                if (cbd->conn != NULL) {
+                       gsize max_size = rspamd_map_effective_max_size(cbd);
+
+                       if (max_size > 0) {
+                               rspamd_http_connection_set_max_size(cbd->conn, max_size);
+                       }
                        /* Apply optional staged timeouts and keepalive tuning */
                        if (cbd->data->connect_timeout > 0 || cbd->data->ssl_timeout > 0 ||
                                cbd->data->write_timeout > 0 || cbd->data->read_timeout > 0) {
@@ -2481,6 +2529,11 @@ check:
                        addr);
 
                if (cbd->conn != NULL) {
+                       gsize max_size = rspamd_map_effective_max_size(cbd);
+
+                       if (max_size > 0) {
+                               rspamd_http_connection_set_max_size(cbd->conn, max_size);
+                       }
                        /* Apply optional staged timeouts and keepalive tuning */
                        if (cbd->data->connect_timeout > 0 || cbd->data->ssl_timeout > 0 ||
                                cbd->data->write_timeout > 0 || cbd->data->read_timeout > 0) {
@@ -3437,6 +3490,9 @@ rspamd_map_parse_backend(struct rspamd_config *cfg, const char *map_line)
                                opt = ucl_object_lookup_any(src,
                                                                                        "max_reuse", "max-reuse", "keepalive_max_reuse", NULL);
                                if (opt) hdata->max_reuse = (unsigned int) ucl_object_toint(opt);
+                               opt = ucl_object_lookup_any(src,
+                                                                                       "max_size", "max-size", NULL);
+                               if (opt) hdata->max_size = (gsize) ucl_object_toint(opt);
                        }
                }
 
index 72a42870a17a27dfda02c96f27d747ff65199af0..7aa6e665e32be9fceb8296971c12b820d52cb6f8 100644 (file)
@@ -127,6 +127,8 @@ struct http_map_data {
        double connection_ttl;
        double idle_timeout;
        unsigned int max_reuse;
+       /* Optional per-map size limit; 0 means use cfg->max_map_size */
+       gsize max_size;
 };
 
 struct static_map_data {
@@ -201,11 +203,11 @@ struct rspamd_map {
        double poll_timeout;
        time_t next_check;
        bool active_http;
-       bool non_trivial;  /* E.g. has http backends in active mode */
-       bool file_only;    /* No HTTP backends found */
-       bool static_only;  /* No need to check */
-       bool no_file_read; /* Do not read files, pass filename to consumer */
-       bool seen;         /* This map has already been watched or pre-loaded */
+       bool non_trivial;          /* E.g. has http backends in active mode */
+       bool file_only;            /* No HTTP backends found */
+       bool static_only;          /* No need to check */
+       bool no_file_read;         /* Do not read files, pass filename to consumer */
+       bool seen;                 /* This map has already been watched or pre-loaded */
        gsize no_file_read_offset; /* Payload offset when consumer mmaps the file (0 for file, 4096 for HTTP cache) */
        /* Shared lock for temporary disabling of map reading (e.g. when this map is written by UI) */
        struct rspamd_map_shared_data *shared;