From: Vsevolod Stakhov Date: Fri, 3 Oct 2025 21:00:44 +0000 (+0100) Subject: [Fix] Enforce server-controlled HTTP map refresh intervals X-Git-Tag: 3.13.2~6^2~2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f8960c2d8153c59ceee3addeb3be9d0b8deaf24d;p=thirdparty%2Frspamd.git [Fix] Enforce server-controlled HTTP map refresh intervals Prevent aggressive HTTP map polling by implementing proper interval bounds: - Cap absurdly high Expires headers (>8h) to min(map_interval * 10, 8h) - Enforce configured map_interval as minimum when server requests faster refresh - Apply 10 minute minimum interval when no Expires header and low map_interval - Simplify logic by consolidating interval calculation in single function This change ensures servers can control refresh rates and prevents clients from causing issues with overly aggressive polling behavior. --- diff --git a/src/libserver/maps/map.c b/src/libserver/maps/map.c index d413870669..c7c5f9839f 100644 --- a/src/libserver/maps/map.c +++ b/src/libserver/maps/map.c @@ -305,10 +305,10 @@ rspamd_map_cache_cb(struct ev_loop *loop, ev_timer *w, int revents) /** * Calculate next check time with proper priority for different cache validation mechanisms - * Priority: ETags > Last-Modified > Cache expiration headers + * Enforces server-controlled refresh intervals to prevent aggressive client polling * @param now current time - * @param expires time from cache expiration header - * @param map_check_interval base polling interval + * @param expires time from cache expiration header (0 if not present) + * @param map_check_interval base polling interval in seconds * @param has_etag whether we have ETag for conditional requests * @param has_last_modified whether we have Last-Modified for conditional requests * @return next check time @@ -322,82 +322,74 @@ rspamd_http_map_process_next_check(struct rspamd_map *map, gboolean has_etag, gboolean has_last_modified) { - static const time_t interval_mult = 4; /* Reduced from 16 to be more responsive */ - static const time_t min_respectful_interval = 5; + static const time_t max_expires_interval = 8 * 3600; /* 8 hours maximum */ + static const time_t min_no_expires_interval = 10 * 60; /* 10 minutes minimum when no expires */ + static const time_t liberal_mult = 10; /* Multiplier for liberal interval */ time_t next_check; - time_t effective_interval = map_check_interval; /* - * Priority order for cache validation: - * 1. ETags (most reliable) - * 2. Last-Modified dates - * 3. Cache expiration headers (least reliable) + * Goal: Respect server-provided expiration while preventing abuse + * Server controls refresh rate via Expires header, client cannot override aggressively */ - if (has_etag || has_last_modified) { - /* - * If we have ETags or Last-Modified, we can use conditional requests - * to avoid unnecessary downloads. However, we still need to be respectful - * to servers and not DoS them with overly aggressive polling. - */ - if (map_check_interval < min_respectful_interval) { + if (expires > now) { + /* Server provided an Expires header */ + time_t expires_interval = expires - now; + + if (expires_interval > max_expires_interval) { /* - * User configured very aggressive polling, but server provides cache validation. - * Enforce minimum respectful interval to avoid DoS'ing the server. + * Absurdly high expiration (> 8 hours) + * Use min(map_check_interval * 10, 8 hours) */ - effective_interval = min_respectful_interval * interval_mult; - msg_info_map("map polling interval %d too aggressive with server cache support for %s, " - "using %d seconds minimum", - (int) map_check_interval, bk->uri, (int) effective_interval); + time_t liberal_interval = map_check_interval * liberal_mult; + if (liberal_interval > max_expires_interval) { + next_check = now + max_expires_interval; + msg_info_map("expires header too high (%d hours) for %s, capping to 8 hours", + (int) (expires_interval / 3600), bk->uri); + } + else { + next_check = now + liberal_interval; + msg_info_map("expires header very high (%d hours) for %s, using liberal interval %d seconds", + (int) (expires_interval / 3600), bk->uri, (int) liberal_interval); + } } - - if (expires > now && (expires - now) <= effective_interval * interval_mult) { - /* Use expires header if it's reasonable (within interval_mult x poll interval) */ - next_check = expires; + else if (expires_interval < map_check_interval) { + /* + * Server wants faster refresh than configured interval + * Respect the configured minimum to prevent abuse + */ + next_check = now + map_check_interval; + msg_debug_map("expires header (%d sec) less than map_check_interval (%d sec) for %s, " + "using map_check_interval", + (int) expires_interval, (int) map_check_interval, bk->uri); } else { - /* Use effective interval, don't extend too much */ - next_check = now + effective_interval; + /* + * Reasonable expires header (between map_check_interval and 8 hours) + * Use it as-is + */ + next_check = expires; } } - else if (expires > now) { + else { /* - * No ETags or Last-Modified available, rely on cache expiration. - * But still cap the interval to avoid too long delays. - * No need for respectful interval protection here since no conditional requests. + * No expires header (or expired) + * Enforce minimum interval to prevent aggressive polling */ - if (expires - now > map_check_interval * interval_mult) { - next_check = now + map_check_interval * interval_mult; + if (map_check_interval < min_no_expires_interval) { + next_check = now + min_no_expires_interval; + msg_info_map("no expires header and low map_check_interval (%d sec) for %s, " + "enforcing 10 minute minimum", + (int) map_check_interval, bk->uri); } else { - next_check = expires; + next_check = now + map_check_interval; } } - else { - /* No valid cache information, check immediately */ - next_check = now; - } return next_check; } -/** - * Calculate respectful polling interval to avoid DoS'ing servers with cache validation - * @param map_check_interval user configured interval - * @return effective interval that respects server resources - */ -static inline time_t -rspamd_map_get_respectful_interval(time_t map_check_interval) -{ - static const time_t min_respectful_interval = 5; /* Minimum 5 seconds to be respectful */ - static const time_t interval_mult = 4; /* Multiplier for respectful minimum */ - - if (map_check_interval < min_respectful_interval) { - return min_respectful_interval * interval_mult; - } - return map_check_interval; -} - static gboolean rspamd_map_secretbox_decrypt_buf(struct rspamd_map_backend *bk, const unsigned char *in, @@ -648,14 +640,11 @@ http_map_finish(struct rspamd_http_connection *conn, } } else if (etag_hdr != NULL || msg->last_modified != 0) { - /* No expires header, but we have ETag or Last-Modified - use respectful interval */ - time_t effective_interval = rspamd_map_get_respectful_interval(map->poll_timeout); - if (effective_interval != map->poll_timeout) { - msg_info_map("map polling interval %d too aggressive with server cache support, " - "using %d seconds minimum", - (int) map->poll_timeout, (int) effective_interval); - } - map->next_check = msg->date + effective_interval; + /* No expires header, but we have ETag or Last-Modified */ + map->next_check = rspamd_http_map_process_next_check(map, bk, msg->date, 0, + (time_t) map->poll_timeout, + etag_hdr != NULL, + msg->last_modified != 0); } if (etag_hdr) { @@ -844,14 +833,11 @@ http_map_finish(struct rspamd_http_connection *conn, } } else if (cbd->data->etag != NULL || msg->last_modified != 0) { - /* No expires header, but we have ETag or Last-Modified - use respectful interval */ - time_t effective_interval = rspamd_map_get_respectful_interval(map->poll_timeout); - if (effective_interval != map->poll_timeout) { - msg_info_map("map polling interval %d too aggressive with server cache support, " - "using %d seconds minimum", - (int) map->poll_timeout, (int) effective_interval); - } - map->next_check = msg->date + effective_interval; + /* No expires header, but we have ETag or Last-Modified */ + map->next_check = rspamd_http_map_process_next_check(map, bk, msg->date, 0, + (time_t) map->poll_timeout, + cbd->data->etag != NULL, + msg->last_modified != 0); } etag_hdr = rspamd_http_message_find_header(msg, "ETag");