]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] protocol: apply inline metadata.settings on /checkv3
authorVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 24 Apr 2026 06:43:54 +0000 (07:43 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 24 Apr 2026 06:43:54 +0000 (07:43 +0100)
Inline metadata.settings on /checkv3 was stashed on task->settings
directly, skipping the apply pipeline. As a result every documented
"apply" effect (action thresholds, symbols / symbols_enabled /
symbols_disabled, subject, variables, add/remove headers) silently
no-op'd, while the same payload sent via the /checkv2 Settings HTTP
header worked. Fixes #5999.

Serialize the inline UCL object to compact JSON and synthesize a
Settings request header so the existing settings.lua
check_query_settings -> apply_settings -> {task:set_settings,
apply_settings_side_effects} pipeline runs uniformly with v2. One
source of truth, no duplicated apply logic in C. Also update the
"settings will be merged" log to "inline settings will take
precedence" since that is what settings.lua actually does when both
settings_id and inline settings are present.

Audit-companion fix: metadata.deliver_to now goes through
rspamd_protocol_escape_braces too, matching the v2 Deliver-To header
which has stripped <...> braces since the beginning.

Add two regression tests in 430_checkv3.robot covering the Lua-side
side effect path (settings.symbols injection) and the C-side action
threshold path (actions.reject override).

src/libserver/protocol.c
test/functional/cases/001_merged/430_checkv3.robot

index 57c6b850a82b9e522ab18e628059b5ebe5276c04..b795d9a2f6b7cb5c168190c9ba4c24b0d93dfa0c 100644 (file)
@@ -2429,7 +2429,14 @@ rspamd_protocol_handle_metadata(struct rspamd_task *task,
        /* deliver_to */
        elt = ucl_object_lookup(metadata, "deliver_to");
        if (elt && ucl_object_type(elt) == UCL_STRING) {
-               task->deliver_to = rspamd_mempool_strdup(task->task_pool, ucl_object_tostring(elt));
+               size_t deliver_len;
+               const char *deliver_str = ucl_object_tolstring(elt, &deliver_len);
+
+               if (deliver_len > 0) {
+                       rspamd_ftok_t deliver_tok = {.begin = deliver_str, .len = deliver_len};
+                       /* Match v2 Deliver-To header semantics: strip <...> braces */
+                       task->deliver_to = rspamd_protocol_escape_braces(task, &deliver_tok);
+               }
        }
 
        /* settings_id */
@@ -2444,9 +2451,40 @@ rspamd_protocol_handle_metadata(struct rspamd_task *task,
        if (elt && ucl_object_type(elt) == UCL_OBJECT) {
                if (task->settings_elt) {
                        msg_info_protocol("both settings_id and inline settings present, "
-                                                         "settings will be merged");
+                                                         "inline settings will take precedence");
+               }
+
+               /*
+                * Serialize the inline UCL settings to JSON and synthesize a 'settings'
+                * request header so the existing settings.lua check_query_settings
+                * pipeline picks them up and runs the apply path uniformly with the v2
+                * Settings HTTP header. Without this, action thresholds, symbols
+                * enable/disable lists, subject rewriting, etc. would never take
+                * effect on /checkv3.
+                */
+               size_t json_len = 0;
+               unsigned char *json = ucl_object_emit_len(elt, UCL_EMIT_JSON_COMPACT,
+                                                                                                 &json_len);
+
+               if (json && json_len > 0) {
+                       char *val_dup = rspamd_mempool_alloc(task->task_pool, json_len);
+                       memcpy(val_dup, json, json_len);
+
+                       rspamd_ftok_t *name_tok = rspamd_mempool_alloc(task->task_pool,
+                                                                                                                  sizeof(*name_tok));
+                       rspamd_ftok_t *val_tok = rspamd_mempool_alloc(task->task_pool,
+                                                                                                                 sizeof(*val_tok));
+
+                       RSPAMD_FTOK_ASSIGN(name_tok, SETTINGS_HEADER);
+                       val_tok->begin = val_dup;
+                       val_tok->len = json_len;
+
+                       rspamd_task_add_request_header(task, name_tok, val_tok);
+               }
+
+               if (json) {
+                       free(json);
                }
-               task->settings = ucl_object_ref(elt);
        }
 
        /* tls.cipher - sets SSL flag */
index 92c66a3f62c1325fd4216ca1b9a17c4dba4bd294..45429b88a286aa5835741778ba4dec9f5c2a83fc 100644 (file)
@@ -27,6 +27,22 @@ checkv3 with settings_id
   Scan File V3  ${GTUBE}  metadata=${meta}
   Expect Symbol  GTUBE
 
+checkv3 inline metadata.settings injects symbol
+  [Documentation]  Inline metadata.settings must run apply_settings_side_effects
+  ...              so settings.symbols actually fires (issue #5999)
+  ${settings_obj} =  Evaluate  {"symbols": {"INLINE_V3_TEST": 1.0}}
+  &{meta} =  Create Dictionary  settings=${settings_obj}
+  Scan File V3  ${GTUBE}  metadata=${meta}
+  Expect Symbol  INLINE_V3_TEST
+
+checkv3 inline metadata.settings overrides reject threshold
+  [Documentation]  Inline metadata.settings.actions must invoke the C-side
+  ...              action threshold apply path (issue #5999)
+  ${settings_obj} =  Evaluate  {"actions": {"reject": 1.0}}
+  &{meta} =  Create Dictionary  settings=${settings_obj}
+  Scan File V3  ${GTUBE}  metadata=${meta}
+  Expect Action  reject
+
 checkv3 missing metadata part
   [Documentation]  Send only message part without metadata, expect HTTP 500 (400 error mapped to 5xx)
   ${status} =  Scan File V3 Single Part  message  test message body